Merge from upstream

This commit is contained in:
401Unauthorized 2023-01-24 15:16:59 +08:00
commit 912686a299
No known key found for this signature in database
51 changed files with 2194 additions and 1068 deletions

View file

@ -32,6 +32,10 @@ if (! exists) {
process.exit(1); process.exit(1);
} }
/**
* Commit updated files
* @param {string} version Version to update to
*/
function commit(version) { function commit(version) {
let msg = "Update to " + version; let msg = "Update to " + version;
@ -47,6 +51,10 @@ function commit(version) {
console.log(res.stdout.toString().trim()); console.log(res.stdout.toString().trim());
} }
/**
* Create a tag with the specified version
* @param {string} version Tag to create
*/
function tag(version) { function tag(version) {
let res = childProcess.spawnSync("git", [ "tag", version ]); let res = childProcess.spawnSync("git", [ "tag", version ]);
console.log(res.stdout.toString().trim()); console.log(res.stdout.toString().trim());
@ -55,6 +63,11 @@ function tag(version) {
console.log(res.stdout.toString().trim()); console.log(res.stdout.toString().trim());
} }
/**
* Check if a tag exists for the specified version
* @param {string} version Version to check
* @returns {boolean} Does the tag already exist
*/
function tagExists(version) { function tagExists(version) {
if (! version) { if (! version) {
throw new Error("invalid version"); throw new Error("invalid version");

View file

@ -25,6 +25,10 @@ if (platform === "linux/amd64") {
const file = fs.createWriteStream("cloudflared.deb"); const file = fs.createWriteStream("cloudflared.deb");
get("https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-" + arch + ".deb"); get("https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-" + arch + ".deb");
/**
* Download specified file
* @param {string} url URL to request
*/
function get(url) { function get(url) {
http.get(url, function (res) { http.get(url, function (res) {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {

View file

@ -43,6 +43,11 @@ const main = async () => {
console.log("Finished."); console.log("Finished.");
}; };
/**
* Ask question of user
* @param {string} question Question to ask
* @returns {Promise<string>} Users response
*/
function question(question) { function question(question) {
return new Promise((resolve) => { return new Promise((resolve) => {
rl.question(question, (answer) => { rl.question(question, (answer) => {

View file

@ -53,6 +53,11 @@ const main = async () => {
console.log("Finished."); console.log("Finished.");
}; };
/**
* Ask question of user
* @param {string} question Question to ask
* @returns {Promise<string>} Users response
*/
function question(question) { function question(question) {
return new Promise((resolve) => { return new Promise((resolve) => {
rl.question(question, (answer) => { rl.question(question, (answer) => {

View file

@ -135,6 +135,11 @@ server.listen({
udp: 5300 udp: 5300
}); });
/**
* Get human readable request type from request code
* @param {number} code Request code to translate
* @returns {string} Human readable request type
*/
function type(code) { function type(code) {
for (let name in Packet.TYPE) { for (let name in Packet.TYPE) {
if (Packet.TYPE[name] === code) { if (Packet.TYPE[name] === code) {

View file

@ -11,6 +11,7 @@ class SimpleMqttServer {
this.port = port; this.port = port;
} }
/** Start the MQTT server */
start() { start() {
this.server.listen(this.port, () => { this.server.listen(this.port, () => {
console.log("server started and listening on port ", this.port); console.log("server started and listening on port ", this.port);

View file

@ -36,10 +36,8 @@ if (! exists) {
} }
/** /**
* Updates the version number in package.json and commits it to git. * Commit updated files
* @param {string} version - The new version number * @param {string} version Version to update to
*
* Generated by Trelent
*/ */
function commit(version) { function commit(version) {
let msg = "Update to " + version; let msg = "Update to " + version;
@ -53,16 +51,19 @@ function commit(version) {
} }
} }
/**
* Create a tag with the specified version
* @param {string} version Tag to create
*/
function tag(version) { function tag(version) {
let res = childProcess.spawnSync("git", [ "tag", version ]); let res = childProcess.spawnSync("git", [ "tag", version ]);
console.log(res.stdout.toString().trim()); console.log(res.stdout.toString().trim());
} }
/** /**
* Checks if a given version is already tagged in the git repository. * Check if a tag exists for the specified version
* @param {string} version - The version to check for. * @param {string} version Version to check
* * @returns {boolean} Does the tag already exist
* Generated by Trelent
*/ */
function tagExists(version) { function tagExists(version) {
if (! version) { if (! version) {

View file

@ -10,6 +10,10 @@ if (!newVersion) {
updateWiki(newVersion); updateWiki(newVersion);
/**
* Update the wiki with new version number
* @param {string} newVersion Version to update to
*/
function updateWiki(newVersion) { function updateWiki(newVersion) {
const wikiDir = "./tmp/wiki"; const wikiDir = "./tmp/wiki";
const howToUpdateFilename = "./tmp/wiki/🆙-How-to-Update.md"; const howToUpdateFilename = "./tmp/wiki/🆙-How-to-Update.md";
@ -39,6 +43,10 @@ function updateWiki(newVersion) {
safeDelete(wikiDir); safeDelete(wikiDir);
} }
/**
* Check if a directory exists and then delete it
* @param {string} dir Directory to delete
*/
function safeDelete(dir) { function safeDelete(dir) {
if (fs.existsSync(dir)) { if (fs.existsSync(dir)) {
fs.rm(dir, { fs.rm(dir, {

1865
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "uptime-kuma", "name": "uptime-kuma",
"version": "1.19.4", "version": "1.19.6",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@ -39,7 +39,7 @@
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain", "build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push", "build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push",
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain", "upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
"setup": "git checkout 1.19.4 && npm ci --production && npm run download-dist", "setup": "git checkout 1.19.6 && npm ci --production && npm run download-dist",
"download-dist": "node extra/download-dist.js", "download-dist": "node extra/download-dist.js",
"mark-as-nightly": "node extra/mark-as-nightly.js", "mark-as-nightly": "node extra/mark-as-nightly.js",
"reset-password": "node extra/reset-password.js", "reset-password": "node extra/reset-password.js",
@ -67,7 +67,7 @@
}, },
"dependencies": { "dependencies": {
"@grpc/grpc-js": "~1.7.3", "@grpc/grpc-js": "~1.7.3",
"@louislam/ping": "~0.4.2-mod.0", "@louislam/ping": "~0.4.2-mod.1",
"@louislam/sqlite3": "15.1.2", "@louislam/sqlite3": "15.1.2",
"args-parser": "~1.3.0", "args-parser": "~1.3.0",
"axios": "~0.27.0", "axios": "~0.27.0",
@ -110,7 +110,7 @@
"prom-client": "~13.2.0", "prom-client": "~13.2.0",
"prometheus-api-metrics": "~3.2.1", "prometheus-api-metrics": "~3.2.1",
"protobufjs": "~7.1.1", "protobufjs": "~7.1.1",
"redbean-node": "0.1.4", "redbean-node": "~0.2.0",
"redis": "~4.5.1", "redis": "~4.5.1",
"socket.io": "~4.5.3", "socket.io": "~4.5.3",
"socket.io-client": "~4.5.3", "socket.io-client": "~4.5.3",

View file

@ -63,6 +63,12 @@ function myAuthorizer(username, password, callback) {
}); });
} }
/**
* Use basic auth if auth is not disabled
* @param {express.Request} req Express request object
* @param {express.Response} res Express response object
* @param {express.NextFunction} next
*/
exports.basicAuth = async function (req, res, next) { exports.basicAuth = async function (req, res, next) {
const middleware = basicAuth({ const middleware = basicAuth({
authorizer: myAuthorizer, authorizer: myAuthorizer,

View file

@ -37,6 +37,10 @@ class CacheableDnsHttpAgent {
this.enable = isEnable; this.enable = isEnable;
} }
/**
* Attach cacheable to HTTP agent
* @param {http.Agent} agent Agent to install
*/
static install(agent) { static install(agent) {
this.cacheable.install(agent); this.cacheable.install(agent);
} }

View file

@ -32,6 +32,7 @@ const initBackgroundJobs = function (args) {
return bree; return bree;
}; };
/** Stop all background jobs if running */
const stopBackgroundJobs = function () { const stopBackgroundJobs = function () {
if (bree) { if (bree) {
bree.stop(); bree.stop();

View file

@ -112,6 +112,11 @@ class Maintenance extends BeanModel {
return this.toPublicJSON(timezone); return this.toPublicJSON(timezone);
} }
/**
* Get a list of weekdays that the maintenance is active for
* Monday=1, Tuesday=2 etc.
* @returns {number[]} Array of active weekdays
*/
getDayOfWeekList() { getDayOfWeekList() {
log.debug("timeslot", "List: " + this.weekdays); log.debug("timeslot", "List: " + this.weekdays);
return JSON.parse(this.weekdays).sort(function (a, b) { return JSON.parse(this.weekdays).sort(function (a, b) {
@ -119,12 +124,20 @@ class Maintenance extends BeanModel {
}); });
} }
/**
* Get a list of days in month that maintenance is active for
* @returns {number[]} 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) {
return a - b; return a - b;
}); });
} }
/**
* Get the start date and time for maintenance
* @returns {dayjs.Dayjs} Start date and time
*/
getStartDateTime() { getStartDateTime() {
let startOfTheDay = dayjs.utc(this.start_date).format("HH:mm"); let startOfTheDay = dayjs.utc(this.start_date).format("HH:mm");
log.debug("timeslot", "startOfTheDay: " + startOfTheDay); log.debug("timeslot", "startOfTheDay: " + startOfTheDay);
@ -137,6 +150,10 @@ class Maintenance extends BeanModel {
return dayjs.utc(this.start_date).add(startTimeSecond, "second"); return dayjs.utc(this.start_date).add(startTimeSecond, "second");
} }
/**
* Get the duraction of maintenance in seconds
* @returns {number} Duration of maintenance
*/
getDuration() { getDuration() {
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
@ -146,6 +163,12 @@ class Maintenance extends BeanModel {
return duration; return duration;
} }
/**
* Convert data from socket to bean
* @param {Bean} bean Bean to fill in
* @param {Object} obj Data to fill bean with
* @returns {Bean} Filled bean
*/
static jsonToBean(bean, obj) { static jsonToBean(bean, obj) {
if (obj.id) { if (obj.id) {
bean.id = obj.id; bean.id = obj.id;

View file

@ -6,6 +6,11 @@ const { UptimeKumaServer } = require("../uptime-kuma-server");
class MaintenanceTimeslot extends BeanModel { 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() { async toPublicJSON() {
const serverTimezoneOffset = UptimeKumaServer.getInstance().getTimezoneOffset(); const serverTimezoneOffset = UptimeKumaServer.getInstance().getTimezoneOffset();
@ -21,6 +26,10 @@ class MaintenanceTimeslot extends BeanModel {
return obj; return obj;
} }
/**
* Return an object that ready to parse to JSON
* @returns {Object}
*/
async toJSON() { async toJSON() {
return await this.toPublicJSON(); return await this.toPublicJSON();
} }

View file

@ -38,7 +38,6 @@ class Monitor extends BeanModel {
id: this.id, id: this.id,
name: this.name, name: this.name,
sendUrl: this.sendUrl, sendUrl: this.sendUrl,
maintenance: await Monitor.isUnderMaintenance(this.id),
}; };
if (this.sendUrl) { if (this.sendUrl) {
@ -496,13 +495,17 @@ class Monitor extends BeanModel {
const options = { const options = {
url: `/containers/${this.docker_container}/json`, url: `/containers/${this.docker_container}/json`,
timeout: this.interval * 1000 * 0.8,
headers: { headers: {
"Accept": "*/*", "Accept": "*/*",
"User-Agent": "Uptime-Kuma/" + version, "User-Agent": "Uptime-Kuma/" + version,
}, },
httpsAgent: new https.Agent({ httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940) maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: ! this.getIgnoreTls(), rejectUnauthorized: !this.getIgnoreTls(),
}),
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
maxCachedSessions: 0,
}), }),
}; };
@ -765,6 +768,13 @@ class Monitor extends BeanModel {
} }
} }
/**
* Make a request using axios
* @param {Object} options Options for Axios
* @param {boolean} finalCall Should this be the final call i.e
* don't retry on faliure
* @returns {Object} Axios response
*/
async makeAxiosRequest(options, finalCall = false) { async makeAxiosRequest(options, finalCall = false) {
try { try {
let res; let res;
@ -1246,6 +1256,7 @@ class Monitor extends BeanModel {
return maintenance.count !== 0; return maintenance.count !== 0;
} }
/** Make sure monitor interval is between bounds */
validate() { validate() {
if (this.interval > MAX_INTERVAL_SECOND) { if (this.interval > MAX_INTERVAL_SECOND) {
throw new Error(`Interval cannot be more than ${MAX_INTERVAL_SECOND} seconds`); throw new Error(`Interval cannot be more than ${MAX_INTERVAL_SECOND} seconds`);

View file

@ -8,6 +8,14 @@ class PromoSMS extends NotificationProvider {
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully."; let okMsg = "Sent Successfully.";
if (notification.promosmsAllowLongSMS === undefined) {
notification.promosmsAllowLongSMS = false;
}
//TODO: Add option for enabling special characters. It will decrese message max length from 160 to 70 chars.
//Lets remove non ascii char
let cleanMsg = msg.replace(/[^\x00-\x7F]/g, "");
try { try {
let config = { let config = {
headers: { headers: {
@ -18,8 +26,9 @@ class PromoSMS extends NotificationProvider {
}; };
let data = { let data = {
"recipients": [ notification.promosmsPhoneNumber ], "recipients": [ notification.promosmsPhoneNumber ],
//Lets remove non ascii char //Trim message to maximum length of 1 SMS or 4 if we allowed long messages
"text": msg.replace(/[^\x00-\x7F]/g, ""), "text": notification.promosmsAllowLongSMS ? cleanMsg.substring(0, 639) : cleanMsg.substring(0, 159),
"long-sms": notification.promosmsAllowLongSMS,
"type": Number(notification.promosmsSMSType), "type": Number(notification.promosmsSMSType),
"sender": notification.promosmsSenderName "sender": notification.promosmsSenderName
}; };

View file

@ -10,7 +10,7 @@ class Pushover extends NotificationProvider {
let pushoverlink = "https://api.pushover.net/1/messages.json"; let pushoverlink = "https://api.pushover.net/1/messages.json";
let data = { let data = {
"message": "<b>Message</b>:" + msg, "message": msg,
"user": notification.pushoveruserkey, "user": notification.pushoveruserkey,
"token": notification.pushoverapptoken, "token": notification.pushoverapptoken,
"sound": notification.pushoversounds, "sound": notification.pushoversounds,

View file

@ -21,6 +21,12 @@ class ServerChan extends NotificationProvider {
} }
} }
/**
* Get the formatted title for message
* @param {?Object} monitorJSON Monitor details (For Up/Down only)
* @param {?Object} heartbeatJSON Heartbeat details (For Up/Down only)
* @returns {string} Formatted title
*/
checkStatus(heartbeatJSON, monitorJSON) { checkStatus(heartbeatJSON, monitorJSON) {
let title = "UptimeKuma Message"; let title = "UptimeKuma Message";
if (heartbeatJSON != null && heartbeatJSON["status"] === UP) { if (heartbeatJSON != null && heartbeatJSON["status"] === UP) {

View file

@ -0,0 +1,113 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
const { setting } = require("../util-server");
let successMessage = "Sent Successfully.";
class Splunk extends NotificationProvider {
name = "Splunk";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
try {
if (heartbeatJSON == null) {
const title = "Uptime Kuma Alert";
const monitor = {
type: "ping",
url: "Uptime Kuma Test Button",
};
return this.postNotification(notification, title, msg, monitor, "trigger");
}
if (heartbeatJSON.status === UP) {
const title = "Uptime Kuma Monitor ✅ Up";
return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, "recovery");
}
if (heartbeatJSON.status === DOWN) {
const title = "Uptime Kuma Monitor 🔴 Down";
return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, "trigger");
}
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
/**
* Check if result is successful, result code should be in range 2xx
* @param {Object} result Axios response object
* @throws {Error} The status code is not in range 2xx
*/
checkResult(result) {
if (result.status == null) {
throw new Error("Splunk notification failed with invalid response!");
}
if (result.status < 200 || result.status >= 300) {
throw new Error("Splunk notification failed with status code " + result.status);
}
}
/**
* Send the message
* @param {BeanModel} notification Message title
* @param {string} title Message title
* @param {string} body Message
* @param {Object} monitorInfo Monitor details (For Up/Down only)
* @param {?string} eventAction Action event for PagerDuty (trigger, acknowledge, resolve)
* @returns {string}
*/
async postNotification(notification, title, body, monitorInfo, eventAction = "trigger") {
let monitorUrl;
if (monitorInfo.type === "port") {
monitorUrl = monitorInfo.hostname;
if (monitorInfo.port) {
monitorUrl += ":" + monitorInfo.port;
}
} else if (monitorInfo.hostname != null) {
monitorUrl = monitorInfo.hostname;
} else {
monitorUrl = monitorInfo.url;
}
if (eventAction === "recovery") {
if (notification.splunkAutoResolve === "0") {
return "No action required";
}
eventAction = notification.splunkAutoResolve;
} else {
eventAction = notification.splunkSeverity;
}
const options = {
method: "POST",
url: notification.splunkRestURL,
headers: { "Content-Type": "application/json" },
data: {
message_type: eventAction,
state_message: `[${title}] [${monitorUrl}] ${body}`,
entity_display_name: "Uptime Kuma Alert: " + monitorInfo.name,
routing_key: notification.pagerdutyIntegrationKey,
entity_id: "Uptime Kuma/" + monitorInfo.id,
}
};
const baseURL = await setting("primaryBaseURL");
if (baseURL && monitorInfo) {
options.client = "Uptime Kuma";
options.client_url = baseURL + getMonitorRelativeURL(monitorInfo.id);
}
let result = await axios.request(options);
this.checkResult(result);
if (result.statusText != null) {
return "Splunk notification succeed: " + result.statusText;
}
return successMessage;
}
}
module.exports = Splunk;

View file

@ -40,6 +40,7 @@ const Stackfield = require("./notification-providers/stackfield");
const Teams = require("./notification-providers/teams"); const Teams = require("./notification-providers/teams");
const TechulusPush = require("./notification-providers/techulus-push"); const TechulusPush = require("./notification-providers/techulus-push");
const Telegram = require("./notification-providers/telegram"); const Telegram = require("./notification-providers/telegram");
const Splunk = require("./notification-providers/splunk");
const Webhook = require("./notification-providers/webhook"); const Webhook = require("./notification-providers/webhook");
const WeCom = require("./notification-providers/wecom"); const WeCom = require("./notification-providers/wecom");
const GoAlert = require("./notification-providers/goalert"); const GoAlert = require("./notification-providers/goalert");
@ -100,6 +101,7 @@ class Notification {
new Teams(), new Teams(),
new TechulusPush(), new TechulusPush(),
new Telegram(), new Telegram(),
new Splunk(),
new Webhook(), new Webhook(),
new WeCom(), new WeCom(),
new GoAlert(), new GoAlert(),

View file

@ -99,6 +99,7 @@ class Prometheus {
} }
} }
/** Remove monitor from prometheus */
remove() { remove() {
try { try {
monitorCertDaysRemaining.remove(this.monitorLabelValues); monitorCertDaysRemaining.remove(this.monitorLabelValues);

View file

@ -6,10 +6,10 @@ class UptimeCacheList {
static list = {}; static list = {};
/** /**
* * Get the uptime for a specific period
* @param monitorID * @param {number} monitorID
* @param duration * @param {number} duration
* @return number * @return {number}
*/ */
static getUptime(monitorID, duration) { static getUptime(monitorID, duration) {
if (UptimeCacheList.list[monitorID] && UptimeCacheList.list[monitorID][duration]) { if (UptimeCacheList.list[monitorID] && UptimeCacheList.list[monitorID][duration]) {
@ -20,6 +20,12 @@ class UptimeCacheList {
} }
} }
/**
* Add uptime for specified monitor
* @param {number} monitorID
* @param {number} duration
* @param {number} uptime Uptime to add
*/
static addUptime(monitorID, duration, uptime) { static addUptime(monitorID, duration, uptime) {
log.debug("UptimeCacheList", "addUptime: " + monitorID + " " + duration); log.debug("UptimeCacheList", "addUptime: " + monitorID + " " + duration);
if (!UptimeCacheList.list[monitorID]) { if (!UptimeCacheList.list[monitorID]) {
@ -28,6 +34,10 @@ class UptimeCacheList {
UptimeCacheList.list[monitorID][duration] = uptime; UptimeCacheList.list[monitorID][duration] = uptime;
} }
/**
* Clear cache for specified monitor
* @param {number} monitorID
*/
static clearCache(monitorID) { static clearCache(monitorID) {
log.debug("UptimeCacheList", "clearCache: " + monitorID); log.debug("UptimeCacheList", "clearCache: " + monitorID);
delete UptimeCacheList.list[monitorID]; delete UptimeCacheList.list[monitorID];

View file

@ -86,6 +86,7 @@ class UptimeKumaServer {
this.io = new Server(this.httpServer); this.io = new Server(this.httpServer);
} }
/** Initialise app after the database has been set up */
async initAfterDatabaseReady() { async initAfterDatabaseReady() {
await CacheableDnsHttpAgent.update(); await CacheableDnsHttpAgent.update();
@ -98,6 +99,11 @@ class UptimeKumaServer {
this.generateMaintenanceTimeslotsInterval = setInterval(this.generateMaintenanceTimeslots, 60 * 1000); this.generateMaintenanceTimeslotsInterval = setInterval(this.generateMaintenanceTimeslots, 60 * 1000);
} }
/**
* Send list of monitors to client
* @param {Socket} socket
* @returns {Object} List of monitors
*/
async sendMonitorList(socket) { async sendMonitorList(socket) {
let list = await this.getMonitorJSONList(socket.userID); let list = await this.getMonitorJSONList(socket.userID);
this.io.to(socket.userID).emit("monitorList", list); this.io.to(socket.userID).emit("monitorList", list);
@ -134,6 +140,11 @@ class UptimeKumaServer {
return await this.sendMaintenanceListByUserID(socket.userID); return await this.sendMaintenanceListByUserID(socket.userID);
} }
/**
* Send list of maintenances to user
* @param {number} userID
* @returns {Object}
*/
async sendMaintenanceListByUserID(userID) { async sendMaintenanceListByUserID(userID) {
let list = await this.getMaintenanceJSONList(userID); let list = await this.getMaintenanceJSONList(userID);
this.io.to(userID).emit("maintenanceList", list); this.io.to(userID).emit("maintenanceList", list);
@ -185,6 +196,11 @@ class UptimeKumaServer {
errorLogStream.end(); errorLogStream.end();
} }
/**
* Get the IP of the client connected to the socket
* @param {Socket} socket
* @returns {string}
*/
async getClientIP(socket) { async getClientIP(socket) {
let clientIP = socket.client.conn.remoteAddress; let clientIP = socket.client.conn.remoteAddress;
@ -203,6 +219,12 @@ class UptimeKumaServer {
} }
} }
/**
* Attempt to get the current server timezone
* If this fails, fall back to environment variables and then make a
* guess.
* @returns {string}
*/
async getTimezone() { async getTimezone() {
let timezone = await Settings.get("serverTimezone"); let timezone = await Settings.get("serverTimezone");
if (timezone) { if (timezone) {
@ -214,16 +236,25 @@ class UptimeKumaServer {
} }
} }
/**
* Get the current offset
* @returns {string}
*/
getTimezoneOffset() { getTimezoneOffset() {
return dayjs().format("Z"); return dayjs().format("Z");
} }
/**
* Set the current server timezone and environment variables
* @param {string} timezone
*/
async setTimezone(timezone) { async setTimezone(timezone) {
await Settings.set("serverTimezone", timezone, "general"); await Settings.set("serverTimezone", timezone, "general");
process.env.TZ = timezone; process.env.TZ = timezone;
dayjs.tz.setDefault(timezone); dayjs.tz.setDefault(timezone);
} }
/** Load the timeslots for maintenance */
async generateMaintenanceTimeslots() { async generateMaintenanceTimeslots() {
let list = await R.find("maintenance_timeslot", " generated_next = 0 AND start_date <= DATETIME('now') "); let list = await R.find("maintenance_timeslot", " generated_next = 0 AND start_date <= DATETIME('now') ");
@ -237,6 +268,7 @@ class UptimeKumaServer {
} }
/** Stop the server */
async stop() { async stop() {
clearTimeout(this.generateMaintenanceTimeslotsInterval); clearTimeout(this.generateMaintenanceTimeslotsInterval);
} }

View file

@ -105,7 +105,7 @@ exports.pingAsync = function (hostname, ipv6 = false) {
ping.promise.probe(hostname, { ping.promise.probe(hostname, {
v6: ipv6, v6: ipv6,
min_reply: 1, min_reply: 1,
timeout: 10, deadline: 10,
}).then((res) => { }).then((res) => {
// If ping failed, it will set field to unknown // If ping failed, it will set field to unknown
if (res.alive) { if (res.alive) {
@ -137,7 +137,7 @@ exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
const { port, username, password, interval = 20 } = options; const { port, username, password, interval = 20 } = options;
// Adds MQTT protocol to the hostname if not already present // Adds MQTT protocol to the hostname if not already present
if (!/^(?:http|mqtt)s?:\/\//.test(hostname)) { if (!/^(?:http|mqtt|ws)s?:\/\//.test(hostname)) {
hostname = "mqtt://" + hostname; hostname = "mqtt://" + hostname;
} }
@ -147,10 +147,11 @@ exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
reject(new Error("Timeout")); reject(new Error("Timeout"));
}, interval * 1000 * 0.8); }, interval * 1000 * 0.8);
log.debug("mqtt", "MQTT connecting"); const mqttUrl = `${hostname}:${port}`;
let client = mqtt.connect(hostname, { log.debug("mqtt", `MQTT connecting to ${mqttUrl}`);
port,
let client = mqtt.connect(mqttUrl, {
username, username,
password password
}); });
@ -282,18 +283,23 @@ exports.postgresQuery = function (connectionString, query) {
const client = new Client({ connectionString }); const client = new Client({ connectionString });
client.connect(); client.connect((err) => {
if (err) {
return client.query(query)
.then(res => {
resolve(res);
})
.catch(err => {
reject(err); reject(err);
}) client.end();
.finally(() => { } else {
// Connected here
client.query(query, (err, res) => {
if (err) {
reject(err);
} else {
resolve(res);
}
client.end(); client.end();
}); });
}
});
}); });
}; };

View file

@ -91,11 +91,16 @@ export default {
}, },
methods: { methods: {
/** Confirm deletion of docker host */
deleteConfirm() { deleteConfirm() {
this.modal.hide(); this.modal.hide();
this.$refs.confirmDelete.show(); this.$refs.confirmDelete.show();
}, },
/**
* Show specified docker host
* @param {number} dockerHostID
*/
show(dockerHostID) { show(dockerHostID) {
if (dockerHostID) { if (dockerHostID) {
let found = false; let found = false;
@ -126,6 +131,7 @@ export default {
this.modal.show(); this.modal.show();
}, },
/** Add docker host */
submit() { submit() {
this.processing = true; this.processing = true;
this.$root.getSocket().emit("addDockerHost", this.dockerHost, this.id, (res) => { this.$root.getSocket().emit("addDockerHost", this.dockerHost, this.id, (res) => {
@ -144,6 +150,7 @@ export default {
}); });
}, },
/** Test the docker host */
test() { test() {
this.processing = true; this.processing = true;
this.$root.getSocket().emit("testDockerHost", this.dockerHost, (res) => { this.$root.getSocket().emit("testDockerHost", this.dockerHost, (res) => {
@ -152,6 +159,7 @@ export default {
}); });
}, },
/** Delete this docker host */
deleteDockerHost() { deleteDockerHost() {
this.processing = true; this.processing = true;
this.$root.getSocket().emit("deleteDockerHost", this.id, (res) => { this.$root.getSocket().emit("deleteDockerHost", this.id, (res) => {

View file

@ -16,7 +16,7 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="tag-color" class="form-label">{{ $t("Color") }}</label> <label for="tag-color" class="form-label">{{ $t("color") }}</label>
<div class="d-flex"> <div class="d-flex">
<div class="col-8 pe-1"> <div class="col-8 pe-1">
<vue-multiselect <vue-multiselect

View file

@ -3,6 +3,8 @@
</template> </template>
<script> <script>
import { DOWN, MAINTENANCE, PENDING, UP } from "../util.ts";
export default { export default {
props: { props: {
/** Monitor this represents */ /** Monitor this represents */
@ -24,7 +26,6 @@ export default {
computed: { computed: {
uptime() { uptime() {
if (this.type === "maintenance") { if (this.type === "maintenance") {
return this.$t("statusMaintenance"); return this.$t("statusMaintenance");
} }
@ -39,19 +40,19 @@ export default {
}, },
color() { color() {
if (this.type === "maintenance" || this.monitor.maintenance) { if (this.lastHeartBeat.status === MAINTENANCE) {
return "maintenance"; return "maintenance";
} }
if (this.lastHeartBeat.status === 0) { if (this.lastHeartBeat.status === DOWN) {
return "danger"; return "danger";
} }
if (this.lastHeartBeat.status === 1) { if (this.lastHeartBeat.status === UP) {
return "primary"; return "primary";
} }
if (this.lastHeartBeat.status === 2) { if (this.lastHeartBeat.status === PENDING) {
return "warning"; return "warning";
} }

View file

@ -26,6 +26,10 @@
<label for="promosms-sender-name" class="form-label">{{ $t("promosmsSMSSender") }}</label> <label for="promosms-sender-name" class="form-label">{{ $t("promosmsSMSSender") }}</label>
<input id="promosms-sender-name" v-model="$parent.notification.promosmsSenderName" type="text" minlength="3" maxlength="11" class="form-control"> <input id="promosms-sender-name" v-model="$parent.notification.promosmsSenderName" type="text" minlength="3" maxlength="11" class="form-control">
</div> </div>
<div class="form-check form-switch">
<input id="promosms-allow-long" v-model="$parent.notification.promosmsAllowLongSMS" type="checkbox" class="form-check-input">
<label for="promosms-allow-long" class="form-label">{{ $t("promosmsAllowLongSMS") }}</label>
</div>
</template> </template>
<script> <script>

View file

@ -0,0 +1,32 @@
<template>
<div class="mb-3">
<label for="splunk-rest-url" class="form-label">{{ $t("Splunk Rest URL") }}</label>
<HiddenInput id="splunk-rest-url" v-model="$parent.notification.splunkRestURL" :required="true" autocomplete="false"></HiddenInput>
</div>
<div class="mb-3">
<label for="splunk-severity" class="form-label">{{ $t("Severity") }}</label>
<select id="splunk-severity" v-model="$parent.notification.splunkSeverity" class="form-select">
<option value="INFO">{{ $t("info") }}</option>
<option value="WARNING">{{ $t("warning") }}</option>
<option value="CRITICAL" selected="selected">{{ $t("critical") }}</option>
</select>
</div>
<div class="mb-3">
<label for="splunk-resolve" class="form-label">{{ $t("Auto resolve or acknowledged") }}</label>
<select id="splunk-resolve" v-model="$parent.notification.splunkAutoResolve" class="form-select">
<option value="0" selected="selected">{{ $t("do nothing") }}</option>
<option value="ACKNOWLEDGEMENT">{{ $t("auto acknowledged") }}</option>
<option value="RECOVERY">{{ $t("auto resolve") }}</option>
</select>
</div>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
};
</script>

View file

@ -42,6 +42,11 @@ export default {
HiddenInput, HiddenInput,
}, },
methods: { methods: {
/**
* Get the URL for telegram updates
* @param {string} [mode=masked] Should the token be masked?
* @returns {string} formatted URL
*/
telegramGetUpdatesURL(mode = "masked") { telegramGetUpdatesURL(mode = "masked") {
let token = `<${this.$t("YOUR BOT TOKEN HERE")}>`; let token = `<${this.$t("YOUR BOT TOKEN HERE")}>`;
@ -55,6 +60,8 @@ export default {
return `https://api.telegram.org/bot${token}/getUpdates`; return `https://api.telegram.org/bot${token}/getUpdates`;
}, },
/** Get the telegram chat ID */
async autoGetTelegramChatID() { async autoGetTelegramChatID() {
try { try {
let res = await axios.get(this.telegramGetUpdatesURL("withToken")); let res = await axios.get(this.telegramGetUpdatesURL("withToken"));

View file

@ -44,6 +44,7 @@ import Webhook from "./Webhook.vue";
import WeCom from "./WeCom.vue"; import WeCom from "./WeCom.vue";
import GoAlert from "./GoAlert.vue"; import GoAlert from "./GoAlert.vue";
import ZohoCliq from "./ZohoCliq.vue"; import ZohoCliq from "./ZohoCliq.vue";
import Splunk from "./Splunk.vue";
/** /**
* Manage all notification form. * Manage all notification form.
@ -92,6 +93,7 @@ const NotificationFormList = {
"stackfield": Stackfield, "stackfield": Stackfield,
"teams": Teams, "teams": Teams,
"telegram": Telegram, "telegram": Telegram,
"Splunk": Splunk,
"webhook": Webhook, "webhook": Webhook,
"WeCom": WeCom, "WeCom": WeCom,
"GoAlert": GoAlert, "GoAlert": GoAlert,

View file

@ -191,6 +191,7 @@ export default {
location.reload(); location.reload();
}, },
/** Show confirmation dialog for disable auth */
confirmDisableAuth() { confirmDisableAuth() {
this.$refs.confirmDisableAuth.show(); this.$refs.confirmDisableAuth.show();
}, },

View file

@ -2,6 +2,7 @@ import { createI18n } from "vue-i18n/dist/vue-i18n.esm-browser.prod.js";
import en from "./lang/en.json"; import en from "./lang/en.json";
const languageList = { const languageList = {
"ar-SY": "العربية",
"cs-CZ": "Čeština", "cs-CZ": "Čeština",
"zh-HK": "繁體中文 (香港)", "zh-HK": "繁體中文 (香港)",
"bg-BG": "Български", "bg-BG": "Български",
@ -48,7 +49,7 @@ for (let lang in languageList) {
}; };
} }
const rtlLangs = [ "fa" ]; const rtlLangs = [ "fa", "ar-SY" ];
export const currentLocale = () => localStorage.locale export const currentLocale = () => localStorage.locale
|| languageList[navigator.language] && navigator.language || languageList[navigator.language] && navigator.language

View file

@ -44,6 +44,7 @@ import {
faWrench, faWrench,
faHeartbeat, faHeartbeat,
faFilter, faFilter,
faInfoCircle,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
library.add( library.add(
@ -88,6 +89,7 @@ library.add(
faWrench, faWrench,
faHeartbeat, faHeartbeat,
faFilter, faFilter,
faInfoCircle,
); );
export { FontAwesomeIcon }; export { FontAwesomeIcon };

684
src/languages/ar-SY.js Normal file
View file

@ -0,0 +1,684 @@
export default {
languageName: "العربية",
checkEverySecond: "تحقق من كل {0} ثانية",
retryCheckEverySecond: "أعد محاولة كل {0} ثانية",
resendEveryXTimes: "إعادة تقديم كل {0} مرات",
resendDisabled: "إعادة الالتزام بالتعطيل",
retriesDescription: "الحد الأقصى لإعادة المحاولة قبل تمييز الخدمة على أنها لأسفل وإرسال إشعار",
ignoreTLSError: "تجاهل خطأ TLS/SSL لمواقع HTTPS",
upsideDownModeDescription: "اقلب الحالة رأسًا على عقب. إذا كانت الخدمة قابلة للوصول إلى أسفل.",
maxRedirectDescription: "الحد الأقصى لعدد إعادة التوجيه لمتابعة. ضبط على 0 لتعطيل إعادة التوجيه.",
enableGRPCTls: "السماح لإرسال طلب GRPC مع اتصال TLS",
grpcMethodDescription: "يتم تحويل اسم الطريقة إلى تنسيق Cammelcase مثل Sayhello Check وما إلى ذلك.",
acceptedStatusCodesDescription: "حدد رموز الحالة التي تعتبر استجابة ناجحة.",
Maintenance: "صيانة",
statusMaintenance: "صيانة",
"Schedule maintenance": "جدولة الصيانة",
"Affected Monitors": "الشاشات المتأثرة",
"Pick Affected Monitors...": "اختيار الشاشات المتأثرة ...",
"Start of maintenance": "بداية الصيانة",
"All Status Pages": "جميع صفحات الحالة",
"Select status pages...": "حدد صفحات الحالة ...",
recurringIntervalMessage: "ركض مرة واحدة كل يوم | قم بالتشغيل مرة واحدة كل يوم {0}",
affectedMonitorsDescription: "حدد المراقبين المتأثرة بالصيانة الحالية",
affectedStatusPages: "إظهار رسالة الصيانة هذه على صفحات الحالة المحددة",
atLeastOneMonitor: "حدد شاشة واحدة على الأقل من المتأثرين",
passwordNotMatchMsg: "كلمة المرور المتكررة لا تتطابق.",
notificationDescription: "يجب تعيين الإخطارات إلى شاشة للعمل.",
keywordDescription: "ابحث في الكلمة الرئيسية في استجابة HTML العادية أو JSON. البحث حساس للحالة.",
pauseDashboardHome: "وقفة",
deleteMonitorMsg: "هل أنت متأكد من حذف هذا الشاشة؟",
deleteMaintenanceMsg: "هل أنت متأكد من حذف هذه الصيانة؟",
deleteNotificationMsg: "هل أنت متأكد من حذف هذا الإشعار لجميع الشاشات؟",
dnsPortDescription: "منفذ خادم DNS. الافتراضيات إلى 53. يمكنك تغيير المنفذ في أي وقت.",
resolverserverDescription: "CloudFlare هو الخادم الافتراضي. يمكنك تغيير خادم المحوّل في أي وقت.",
rrtypeDescription: "حدد نوع RR الذي تريد مراقبته",
pauseMonitorMsg: "هل أنت متأكد من أن تتوقف مؤقتًا؟",
enableDefaultNotificationDescription: "سيتم تمكين هذا الإشعار افتراضيًا للشاشات الجديدة. لا يزال بإمكانك تعطيل الإخطار بشكل منفصل لكل شاشة.",
clearEventsMsg: "هل أنت متأكد من حذف جميع الأحداث لهذا الشاشة؟",
clearHeartbeatsMsg: "هل أنت متأكد من حذف جميع دقات القلب لهذا الشاشة؟",
confirmClearStatisticsMsg: "هل أنت متأكد من أنك تريد حذف جميع الإحصائيات؟",
importHandleDescription: "اختر 'تخطي موجود' إذا كنت تريد تخطي كل شاشة أو إشعار بنفس الاسم. 'الكتابة فوق' سوف يحذف كل شاشة وإخطار موجود.",
confirmImportMsg: "هل أنت متأكد من أنك تريد استيراد النسخ الاحتياطي؟ يرجى التحقق من أنك حددت خيار الاستيراد الصحيح.",
twoFAVerifyLabel: "الرجاء إدخال الرمز المميز الخاص بك للتحقق من 2FA",
tokenValidSettingsMsg: "الرمز المميز صالح! يمكنك الآن حفظ إعدادات 2FA.",
confirmEnableTwoFAMsg: "هل أنت متأكد من أنك تريد تمكين 2FA؟",
confirmDisableTwoFAMsg: "هل أنت متأكد من أنك تريد تعطيل 2FA؟",
Settings: "إعدادات",
Dashboard: "لوحة التحكم",
"New Update": "تحديث جديد",
Language: "لغة",
Appearance: "مظهر",
Theme: "سمة",
General: "عام",
"Primary Base URL": "عنوان URL الأساسي",
Version: "الإصدار",
"Check Update On GitHub": "تحقق من التحديث على GitHub",
List: "قائمة",
Add: "يضيف",
"Add New Monitor": "أضف شاشة جديدة",
"Quick Stats": "إحصائيات سريعة",
Up: "فوق",
Down: "أسفل",
Pending: "قيد الانتظار",
Unknown: "غير معرّف",
Pause: "إيقاف مؤقت",
Name: "الاسم",
Status: "الحالة",
DateTime: "الوقت والتاريخ",
Message: "الرسالة",
"No important events": "لا توجد أحداث مهمة",
Resume: "استمرار",
Edit: "تعديل",
Delete: "حذف",
Current: "حالي",
Uptime: "مدة التشغيل",
"Cert Exp.": "تصدير الشهادة",
Monitor: "مراقب | مراقبات",
day: "يوم | أيام",
"-day": "-يوم",
hour: "ساعة",
"-hour": "-ساعة",
Response: "استجاية",
Ping: "بينغ",
"Monitor Type": "نوع المراقب",
Keyword: "كلمة مفتاحية",
"Friendly Name": "اسم معروف",
URL: "عنوان URL",
Hostname: "اسم المضيف",
Port: "المنفذ",
"Heartbeat Interval": "فاصل نبضات القلب",
Retries: "يحاول مجدداً",
"Heartbeat Retry Interval": "الفاصل الزمني لإعادة محاكمة نبضات القلب",
"Resend Notification if Down X times consequently": "إعادة تقديم الإخطار إذا انخفض x مرات بالتالي",
Advanced: "متقدم",
"Upside Down Mode": "وضع أسفل أسفل",
"Max. Redirects": "الأعلى. إعادة التوجيه",
"Accepted Status Codes": "رموز الحالة المقبولة",
"Push URL": "دفع عنوان URL",
needPushEvery: "يجب عليك استدعاء عنوان URL هذا كل ثانية.",
pushOptionalParams: "المعلمات الاختيارية",
Save: "يحفظ",
Notifications: "إشعارات",
"Not available, please setup.": "غير متوفر من فضلك الإعداد.",
"Setup Notification": "إشعار الإعداد",
Light: "نور",
Dark: "داكن",
Auto: "آلي",
"Theme - Heartbeat Bar": "موضوع - بار نبضات",
Normal: "طبيعي",
Bottom: "الأسفل",
None: "لا أحد",
Timezone: "وحدة زمنية",
"Search Engine Visibility": "محرك بحث الرؤية",
"Allow indexing": "السماح الفهرسة",
"Discourage search engines from indexing site": "تثبيط محركات البحث من موقع الفهرسة",
"Change Password": "غير كلمة السر",
"Current Password": "كلمة المرور الحالي",
"New Password": "كلمة سر جديدة",
"Repeat New Password": "كرر كلمة المرور الجديدة",
"Update Password": "تطوير كلمة السر",
"Disable Auth": "تعطيل المصادقة",
"Enable Auth": "تمكين المصادقة",
"disableauth.message1": "هل أنت متأكد من أن <strong> تعطيل المصادقة </strong>؟",
"disableauth.message2": "تم تصميمه للسيناريوهات <strong> حيث تنوي تنفيذ مصادقة الطرف الثالث </strong> أمام كوما في وقت التشغيل مثل CloudFlare Access Authelia أو آليات المصادقة الأخرى.",
"Please use this option carefully!": "الرجاء استخدام هذا الخيار بعناية!",
Logout: "تسجيل خروج",
Leave: "غادر",
"I understand, please disable": "أنا أفهم من فضلك تعطيل",
Confirm: "يتأكد",
Yes: "نعم",
No: "رقم",
Username: "اسم المستخدم",
Password: "كلمة المرور",
"Remember me": "تذكرنى",
Login: "تسجيل الدخول",
"No Monitors, please": "لا شاشات من فضلك",
"add one": "أضف واحدا",
"Notification Type": "نوع إعلام",
Email: "بريد إلكتروني",
Test: "امتحان",
"Certificate Info": "معلومات الشهادة",
"Resolver Server": "خادم Resolver",
"Resource Record Type": "نوع سجل الموارد",
"Last Result": "اخر نتيجة",
"Create your admin account": "إنشاء حساب المسؤول الخاص بك",
"Repeat Password": "اعد كلمة السر",
"Import Backup": "استيراد النسخ الاحتياطي",
"Export Backup": "النسخ الاحتياطي تصدير",
Export: "يصدّر",
Import: "يستورد",
respTime: "resp. الوقت (MS)",
notAvailableShort: "ن/أ",
"Default enabled": "التمكين الافتراضي",
"Apply on all existing monitors": "تنطبق على جميع الشاشات الحالية",
Create: "خلق",
"Clear Data": "امسح البيانات",
Events: "الأحداث",
Heartbeats: "نبضات القلب",
"Auto Get": "الحصول على السيارات",
backupDescription: "يمكنك النسخ الاحتياطي لجميع الشاشات والإشعارات في ملف JSON.",
backupDescription2: "ملحوظة",
backupDescription3: "يتم تضمين البيانات الحساسة مثل الرموز الإخطار في ملف التصدير ؛ يرجى تخزين التصدير بشكل آمن.",
alertNoFile: "الرجاء تحديد ملف للاستيراد.",
alertWrongFileType: "الرجاء تحديد ملف JSON.",
"Clear all statistics": "مسح جميع الإحصاءات",
"Skip existing": "تخطي الموجود",
Overwrite: "الكتابة فوق",
Options: "خيارات",
"Keep both": "احتفظ بكليهما",
"Verify Token": "تحقق من الرمز المميز",
"Setup 2FA": "الإعداد 2FA",
"Enable 2FA": "تمكين 2FA",
"Disable 2FA": "تعطيل 2FA",
"2FA Settings": "2FA إعدادات",
"Two Factor Authentication": "توثيق ذو عاملين",
Active: "نشيط",
Inactive: "غير نشط",
Token: "رمز",
"Show URI": "أظهر URI",
Tags: "العلامات",
"Add New below or Select...": "أضف جديدًا أدناه أو حدد ...",
"Tag with this name already exist.": "علامة مع هذا الاسم موجود بالفعل.",
"Tag with this value already exist.": "علامة مع هذه القيمة موجودة بالفعل.",
color: "اللون",
"value (optional)": "القيمة (اختياري)",
Gray: "رمادي",
Red: "أحمر",
Orange: "البرتقالي",
Green: "لون أخضر",
Blue: "أزرق",
Indigo: "النيلي",
Purple: "نفسجي",
Pink: "لون القرنفل",
Custom: "العادة",
"Search...": "يبحث...",
"Avg. Ping": "متوسط. بينغ",
"Avg. Response": "متوسط. إجابة",
"Entry Page": "صفحة الدخول",
statusPageNothing: "لا شيء هنا الرجاء إضافة مجموعة أو شاشة.",
"No Services": "لا توجد خدمات",
"All Systems Operational": "جميع الأنظمة التشغيلية",
"Partially Degraded Service": "الخدمة المتدهورة جزئيا",
"Degraded Service": "خدمة متدهورة",
"Add Group": "أضف مجموعة",
"Add a monitor": "إضافة شاشة",
"Edit Status Page": "تحرير صفحة الحالة",
"Go to Dashboard": "الذهاب إلى لوحة القيادة",
"Status Page": "صفحة الحالة",
"Status Pages": "صفحات الحالة",
defaultNotificationName: "تنبيه {الإخطار} ({number})",
here: "هنا",
Required: "مطلوب",
telegram: "برقية",
"ZohoCliq": "Zohocliq",
"Bot Token": "رمز الروبوت",
wayToGetTelegramToken: "يمكنك الحصول على رمز من {0}.",
"Chat ID": "معرف الدردشة",
supportTelegramChatID: "دعم الدردشة المباشرة / معرف الدردشة للقناة",
wayToGetTelegramChatID: "يمكنك الحصول على معرف الدردشة الخاص بك عن طريق إرسال رسالة إلى الروبوت والانتقال إلى عنوان URL هذا لعرض Chat_id",
"YOUR BOT TOKEN HERE": "رمز الروبوت الخاص بك هنا",
chatIDNotFound: "لم يتم العثور على معرف الدردشة ؛ الرجاء إرسال رسالة إلى هذا الروبوت أولاً",
webhook: "webhook",
"Post URL": "بعد عنوان URL",
"Content Type": "نوع المحتوى",
webhookJsonDesc: "{0} مفيد لأي خوادم HTTP الحديثة مثل Express.js",
webhookFormDataDesc: "{multipart} مفيد لـ PHP. سيحتاج JSON إلى تحليل {decodefunction}",
webhookAdditionalHeadersTitle: "رؤوس إضافية",
webhookAdditionalHeadersDesc: "يحدد رؤوس إضافية مرسلة مع webhook.",
smtp: "البريد الإلكتروني (SMTP)",
secureOptionNone: "لا شيء / startTls (25 587)",
secureOptionTLS: "TLS (465)",
"Ignore TLS Error": "تجاهل خطأ TLS",
"From Email": "من البريد الإلكترونى",
emailCustomSubject: "موضوع مخصص",
"To Email": "للبريد الإلكتروني",
smtpCC: "نسخة",
smtpBCC: "BCC",
discord: "خلاف",
"Discord Webhook URL": "Discord Webhook URL",
wayToGetDiscordURL: "يمكنك الحصول على هذا عن طريق الانتقال إلى إعدادات الخادم -> التكامل -> إنشاء WebHook",
"Bot Display Name": "اسم عرض الروبوت",
"Prefix Custom Message": "بادئة رسالة مخصصة",
"Hello @everyone is...": "مرحبًا {'@'} الجميع ...",
teams: "فرق Microsoft",
"Webhook URL": "Webhook URL",
wayToGetTeamsURL: "يمكنك معرفة كيفية إنشاء عنوان URL webhook {0}.",
wayToGetZohoCliqURL: "يمكنك معرفة كيفية إنشاء عنوان URL webhook {0}.",
signal: "الإشارة",
Number: "رقم",
Recipients: "المستلمين",
needSignalAPI: "تحتاج إلى وجود عميل إشارة مع REST API.",
wayToCheckSignalURL: "يمكنك التحقق من عنوان URL هذا لعرض كيفية إعداد واحد",
signalImportant: "مهم",
gotify: "gotify",
"Application Token": "رمز التطبيق",
"Server URL": "عنوان URL الخادم",
Priority: "أولوية",
slack: "تثاقل",
"Icon Emoji": "أيقونة الرموز التعبيرية",
"Channel Name": "اسم القناة",
"Uptime Kuma URL": "UPTIME KUMA URL",
aboutWebhooks: "مزيد من المعلومات حول Webhooks ON",
aboutChannelName: "أدخل اسم القناة في حقل اسم القناة {0} إذا كنت تريد تجاوز قناة WebHook. السابق",
aboutKumaURL: "إذا تركت حقل URL في وقت التشغيل KUMA فارغًا ، فسيتم افتراضيًا إلى صفحة GitHub Project.",
emojiCheatSheet: "ورقة الغش في الرموز التعبيرية",
"rocket.chat": "صاروخ",
pushover: "مهمة سهلة",
pushy: "انتهازي",
PushByTechulus: "دفع بواسطة Techulus",
octopush: "أوكتوبوش",
promosms: "الترويجيات",
clicksendsms: "نقرات SMS",
lunasea: "لوناسيا",
apprise: "إبلاغ (دعم 50+ خدمات الإخطار)",
GoogleChat: "دردشة Google",
pushbullet: "حماس",
Kook: "كووك",
wayToGetKookBotToken: "قم بإنشاء تطبيق واحصل على رمز الروبوت الخاص بك على {0}",
wayToGetKookGuildID: "قم بتشغيل 'وضع المطور' في إعداد Kook وانقر بزر الماوس الأيمن على النقابة للحصول على معرفه",
"Guild ID": "معرف النقابة",
line: "خط",
mattermost: "المادة",
"User Key": "مفتاح المستخدم",
Device: "جهاز",
"Message Title": "عنوان الرسالة",
"Notification Sound": "صوت الإشعار",
"More info on": "مزيد من المعلومات حول",
pushoverDesc1: "أولوية الطوارئ (2) لها مهلة افتراضية 30 ثانية بين إعادة المحاولة وستنتهي صلاحيتها بعد ساعة واحدة.",
pushoverDesc2: "إذا كنت ترغب في إرسال إشعارات إلى أجهزة مختلفة ، قم بملء حقل الجهاز.",
"SMS Type": "نوع الرسائل القصيرة",
octopushTypePremium: "قسط (سريع - موصى به للتنبيه)",
octopushTypeLowCost: "التكلفة المنخفضة (بطيئة - تم حظرها أحيانًا بواسطة المشغل)",
checkPrice: "تحقق من الأسعار {0}",
apiCredentials: "بيانات اعتماد API",
octopushLegacyHint: "هل تستخدم الإصدار القديم من Octopush (2011-2020) أو الإصدار الجديد؟",
"Check octopush prices": "تحقق من أسعار Octopush {0}.",
octopushPhoneNumber: "رقم الهاتف (تنسيق intl على سبيل المثال",
octopushSMSSender: "اسم مرسل الرسائل القصيرة",
"LunaSea Device ID": "معرف جهاز Lunasea",
"Apprise URL": "إبلاغ عنوان URL",
"Example": "مثال",
"Read more:": "{0} :قراءة المزيد",
"Status:": "{0} :حالة",
"Read more": "قراءة المزيد",
appriseInstalled: "تم تثبيت Prosise.",
appriseNotInstalled: "الإبرام غير مثبت. {0}",
"Access Token": "رمز وصول",
"Channel access token": "قناة الوصول إلى الرمز",
"Line Developers Console": "تحكم المطورين",
lineDevConsoleTo: "وحدة المطورين Line Console - {0}",
"Basic Settings": "الإعدادات الأساسية",
"User ID": "معرف المستخدم",
"Messaging API": "واجهة برمجة تطبيقات المراسلة",
wayToGetLineChannelToken: "قم أولاً بالوصول إلى {0} إنشاء مزود وقناة (واجهة برمجة تطبيقات المراسلة) ، ثم يمكنك الحصول على رمز الوصول إلى القناة ومعرف المستخدم من عناصر القائمة المذكورة أعلاه.",
"Icon URL": "url url icon",
aboutIconURL: "يمكنك توفير رابط لصورة في \"Icon URL\" لتجاوز صورة الملف الشخصي الافتراضي. لن يتم استخدامه إذا تم تعيين رمز رمز رمز.",
aboutMattermostChannelName: "يمكنك تجاوز القناة الافتراضية التي تنشرها WebHook من خلال إدخال اسم القناة في \"Channel Name\" الحقل. يجب تمكين هذا في إعدادات Webhook Mattern. السابق",
matrix: "مصفوفة",
promosmsTypeEco: "SMS Eco - رخيصة ولكن بطيئة وغالبًا ما تكون محملة. يقتصر فقط على المستفيدين البولنديين.",
promosmsTypeFlash: "SMS Flash - سيتم عرض الرسالة تلقائيًا على جهاز المستلم. يقتصر فقط على المستفيدين البولنديين.",
promosmsTypeFull: "SMS Full - Tier Premium SMS يمكنك استخدام اسم المرسل الخاص بك (تحتاج إلى تسجيل الاسم أولاً). موثوقة للتنبيهات.",
promosmsTypeSpeed: "سرعة الرسائل القصيرة - أولوية قصوى في النظام. سريع وموثوق للغاية ولكنه مكلف (حوالي مرتين من الرسائل القصيرة السعر الكامل).",
promosmsPhoneNumber: "رقم الهاتف (للمستلم البولندي ، يمكنك تخطي رموز المنطقة)",
promosmsSMSSender: "اسم مرسل الرسائل القصيرة",
promosmsAllowLongSMS: "السماح الرسائل القصيرة الطويلة",
"Feishu WebHookUrl": "Feishu Webhookurl",
matrixHomeserverURL: "عنوان URL HomeServer (مع HTTP (S)",
"Internal Room Id": "معرف الغرفة الداخلية",
matrixDesc1: "يمكنك العثور على معرف الغرفة الداخلي من خلال البحث في القسم المتقدم من إعدادات الغرفة في عميل Matrix الخاص بك. يجب أن تبدو مثل! QMDRCPUIFLWSFJXYE6",
matrixDesc2: "يوصى بشدة بإنشاء مستخدم جديد ولا تستخدم رمز الوصول إلى مستخدم Matrix الخاص بك لأنه سيتيح الوصول الكامل إلى حسابك وجميع الغرف التي انضمت إليها. بدلاً من ذلك ، قم بإنشاء مستخدم جديد ودعوته فقط إلى الغرفة التي تريد تلقيها الإشعار فيها. يمكنك الحصول على رمز الوصول عن طريق تشغيل {0}",
Method: "طريقة",
Body: "الجسم",
Headers: "الرؤوس",
PushUrl: "دفع عنوان URL",
HeadersInvalidFormat: "رؤوس الطلبات غير صالحة JSON",
BodyInvalidFormat: "هيئة الطلب غير صالحة JSON",
"Monitor History": "مراقبة التاريخ",
clearDataOlderThan: "الحفاظ على بيانات سجل المراقبة للأيام {0}.",
PasswordsDoNotMatch: "كلمة المرور غير مطابقة.",
records: "السجلات",
"One record": "سجل واحد",
steamApiKeyDescription: "لمراقبة خادم لعبة Steam ، تحتاج إلى مفتاح Steam Web-API. يمكنك تسجيل مفتاح API الخاص بك هنا",
"Current User": "المستخدم الحالي",
topic: "عنوان",
topicExplanation: "موضوع MQTT لرصد",
successMessage: "نجاح رسالة",
successMessageExplanation: "رسالة MQTT التي ستعتبر نجاحًا",
recent: "الأخيرة",
Done: "فعله",
Info: "معلومات",
Security: "حماية",
"Steam API Key": "مفتاح API Steam",
"Shrink Database": "تقلص قاعدة البيانات",
"Pick a RR-Type...": "اختر نوع RR ...",
"Pick Accepted Status Codes...": "اختيار رموز الحالة المقبولة ...",
Default: "تقصير",
"HTTP Options": "خيارات HTTP",
"Create Incident": "إنشاء حادث",
Title: "لقب",
Content: "المحتوى",
Style: "أسلوب",
info: "معلومات",
warning: "تحذير",
danger: "خطر",
error: "خطأ",
critical: "شديد الأهمية",
primary: "الأولية",
light: "نور",
dark: "ظلام",
Post: "بريد",
"Please input title and content": "الرجاء إدخال العنوان والمحتوى",
Created: "مخلوق",
"Last Updated": "التحديث الاخير",
Unpin: "إلغاء",
"Switch to Light Theme": "التبديل إلى موضوع الضوء",
"Switch to Dark Theme": "التبديل إلى موضوع الظلام",
"Show Tags": "أضهر العلامات",
"Hide Tags": "إخفاء العلامات",
Description: "وصف",
"No monitors available.": "لا شاشات المتاحة.",
"Add one": "أضف واحدا",
"No Monitors": "لا شاشات",
"Untitled Group": "مجموعة بلا عنوان",
Services: "خدمات",
Discard: "تجاهل",
Cancel: "يلغي",
"Powered by": "مشغل بواسطة",
shrinkDatabaseDescription: "تشغيل فراغ قاعدة البيانات لـ SQLite. إذا تم إنشاء قاعدة البيانات الخاصة بك بعد تمكين 1.10.0 AUTO_VACUUM بالفعل ولا يلزم هذا الإجراء.",
serwersms: "Serwersms.pl",
serwersmsAPIUser: "اسم مستخدم API (بما في ذلك بادئة WebAPI_)",
serwersmsAPIPassword: "كلمة مرور API",
serwersmsPhoneNumber: "رقم الهاتف",
serwersmsSenderName: "اسم مرسل الرسائل القصيرة (مسجل عبر بوابة العملاء)",
smseagle: "smseagle",
smseagleTo: "أرقام الهواتف)",
smseagleGroup: "اسم مجموعة كتب الهاتف (S)",
smseagleContact: "كتاب الاتصال اسم (S)",
smseagleRecipientType: "نوع المستلم",
smseagleRecipient: "المتلقي (المتلقيين) (يجب فصل المتعددة مع فاصلة)",
smseagleToken: "API وصول الرمز المميز",
smseagleUrl: "عنوان URL لجهاز SMSEGLE الخاص بك",
smseagleEncoding: "إرسال Unicode",
smseaglePriority: "أولوية الرسالة (0-9 افتراضي = 0)",
stackfield: "Stackfield",
Customize: "يعدل أو يكيف",
"Custom Footer": "تذييل مخصص",
"Custom CSS": "لغة تنسيق ويب حسب الطلب",
smtpDkimSettings: "إعدادات DKIM",
smtpDkimDesc: "يرجى الرجوع إلى Nodemailer dkim {0} للاستخدام.",
documentation: "توثيق",
smtpDkimDomain: "اسم النطاق",
smtpDkimKeySelector: "المحدد الرئيسي",
smtpDkimPrivateKey: "مفتاح سري",
smtpDkimHashAlgo: "خوارزمية التجزئة (اختياري)",
smtpDkimheaderFieldNames: "مفاتيح الرأس للتوقيع (اختياري)",
smtpDkimskipFields: "مفاتيح الرأس لا توقيع (اختياري)",
wayToGetPagerDutyKey: "يمكنك الحصول على هذا عن طريق الانتقال إلى الخدمة -> دليل الخدمة -> (حدد خدمة) -> تكامل -> إضافة التكامل. هنا يمكنك البحث عن \"Events API V2\". مزيد من المعلومات {0}",
"Integration Key": "مفتاح التكامل",
"Integration URL": "URL تكامل",
"Auto resolve or acknowledged": "حل السيارات أو الاعتراف به",
"do nothing": "لا تفعل شيئا",
"auto acknowledged": "اعترف السيارات",
"auto resolve": "عزم السيارات",
gorush: "جورش",
alerta: "أليتا",
alertaApiEndpoint: "نقطة نهاية API",
alertaEnvironment: "بيئة",
alertaApiKey: "مفتاح API",
alertaAlertState: "حالة التنبيه",
alertaRecoverState: "استعادة الدولة",
deleteStatusPageMsg: "هل أنت متأكد من حذف صفحة الحالة هذه؟",
Proxies: "وكلاء",
default: "تقصير",
enabled: "تمكين",
setAsDefault: "تعيين كافتراضي",
deleteProxyMsg: "هل أنت متأكد من حذف هذا الوكيل لجميع الشاشات؟",
proxyDescription: "يجب تعيين الوكلاء إلى شاشة للعمل.",
enableProxyDescription: "لن يؤثر هذا الوكيل على طلبات الشاشة حتى يتم تنشيطه. يمكنك التحكم مؤقتًا في تعطيل الوكيل من جميع الشاشات حسب حالة التنشيط.",
setAsDefaultProxyDescription: "سيتم تمكين هذا الوكيل افتراضيًا للشاشات الجديدة. لا يزال بإمكانك تعطيل الوكيل بشكل منفصل لكل شاشة.",
"Certificate Chain": "سلسلة الشهادة",
Valid: "صالح",
Invalid: "غير صالح",
AccessKeyId: "معرف AccessKey",
SecretAccessKey: "Accesskey Secret",
PhoneNumbers: "أرقام الهواتف",
TemplateCode: "TemplateCode",
SignName: "اسم تسجيل الدخول",
"Sms template must contain parameters: ": "يجب أن يحتوي قالب الرسائل القصيرة على معلمات:",
"Bark Endpoint": "نقطة نهاية اللحاء",
"Bark Group": "مجموعة اللحاء",
"Bark Sound": "صوت اللحاء",
WebHookUrl: "webhookurl",
SecretKey: "Secretkey",
"For safety, must use secret key": "للسلامة يجب استخدام المفتاح السري",
"Device Token": "رمز الجهاز",
Platform: "منصة",
iOS: "iOS",
Android: "ذكري المظهر",
Huawei: "هواوي",
High: "عالٍ",
Retry: "إعادة المحاولة",
Topic: "عنوان",
"WeCom Bot Key": "WECOM BOT KEY",
"Setup Proxy": "وكيل الإعداد",
"Proxy Protocol": "بروتوكول الوكيل",
"Proxy Server": "مخدم بروكسي",
"Proxy server has authentication": "خادم الوكيل لديه مصادقة",
User: "المستعمل",
Installed: "المثبتة",
"Not installed": "غير مثبت",
Running: "جري",
"Not running": "لا يعمل",
"Remove Token": "إزالة الرمز المميز",
Start: "بداية",
Stop: "قف",
"Uptime Kuma": "وقت التشغيل كوما",
"Add New Status Page": "أضف صفحة حالة جديدة",
Slug: "سبيكة",
"Accept characters": "قبول الشخصيات",
startOrEndWithOnly: "ابدأ أو ينتهي بـ {0} فقط",
"No consecutive dashes": "لا شرطات متتالية",
Next: "التالي",
"The slug is already taken. Please choose another slug.": "تم أخذ سبيكة بالفعل. الرجاء اختيار سبيكة أخرى.",
"No Proxy": "لا الوكيل",
Authentication: "المصادقة",
"HTTP Basic Auth": "HTTP الأساسي Auth",
"New Status Page": "صفحة حالة جديدة",
"Page Not Found": "الصفحة غير موجودة",
"Reverse Proxy": "وكيل عكسي",
Backup: "دعم",
About: "عن",
wayToGetCloudflaredURL: "(قم بتنزيل CloudFlared من {0})",
cloudflareWebsite: "موقع CloudFlare",
"Message:": ":رسالة",
"Don't know how to get the token? Please read the guide": "لا أعرف كيف تحصل على الرمز المميز؟ يرجى قراءة الدليل",
"The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "قد يضيع الاتصال الحالي إذا كنت تتصل حاليًا عبر نفق CloudFlare. هل أنت متأكد تريد إيقافها؟ اكتب كلمة المرور الحالية لتأكيدها.",
"HTTP Headers": "رؤوس HTTP",
"Trust Proxy": "الوكيل الثقة",
"Other Software": "برامج أخرى",
"For example: nginx, Apache and Traefik.": "على سبيل المثال: nginx و Apache و Traefik.",
"Please read": "يرجى القراءة",
"Subject": "موضوع",
"Valid To": "صالحة ل",
"Days Remaining": "الأيام المتبقية",
"Issuer": "المصدر",
"Fingerprint": "بصمة",
"No status pages": "لا صفحات الحالة",
"Domain Name Expiry Notification": "اسم النطاق إشعار انتهاء الصلاحية",
Proxy: "الوكيل",
"Date Created": "تاريخ الإنشاء",
HomeAssistant: "مساعد المنزل",
onebotHttpAddress: "OneBot HTTP عنوان",
onebotMessageType: "نوع رسالة OneBot",
onebotGroupMessage: "مجموعة",
onebotPrivateMessage: "خاص",
onebotUserOrGroupId: "معرف المجموعة/المستخدم",
onebotSafetyTips: "للسلامة يجب ضبط الرمز المميز للوصول",
"PushDeer Key": "مفتاح PushDeer",
"Footer Text": "نص تذييل",
"Show Powered By": "عرض مدعوم من قبل",
"Domain Names": "أسماء المجال",
signedInDisp: "وقعت في {0}",
signedInDispDisabled: "معاق المصادقة.",
RadiusSecret: "سر نصف القطر",
RadiusSecretDescription: "السر المشترك بين العميل والخادم",
RadiusCalledStationId: "يسمى معرف المحطة",
RadiusCalledStationIdDescription: "معرف الجهاز المتصل",
RadiusCallingStationId: "معرف محطة الاتصال",
RadiusCallingStationIdDescription: "معرف جهاز الاتصال",
"Certificate Expiry Notification": "إشعار انتهاء الصلاحية",
"API Username": "اسم المستخدم API",
"API Key": "مفتاح API",
"Recipient Number": "رقم المستلم",
"From Name/Number": "من الاسم/الرقم",
"Leave blank to use a shared sender number.": "اترك فارغًا لاستخدام رقم المرسل المشترك.",
"Octopush API Version": "إصدار Octopush API",
"Legacy Octopush-DM": "Legacy Octopush-DM",
endpoint: "نقطة النهاية",
octopushAPIKey: "\"API key\" from HTTP API بيانات اعتماد في لوحة التحكم",
octopushLogin: "\"Login\" من بيانات اعتماد API HTTP في لوحة التحكم",
promosmsLogin: "اسم تسجيل الدخول API",
promosmsPassword: "كلمة مرور API",
"pushoversounds pushover": "سداد (افتراضي)",
"pushoversounds bike": "دراجة هوائية",
"pushoversounds bugle": "بوق",
"pushoversounds cashregister": "ماكينة تسجيل المدفوعات النقدية",
"pushoversounds classical": "كلاسيكي",
"pushoversounds cosmic": "كونية",
"pushoversounds falling": "هبوط",
"pushoversounds gamelan": "Gamelan",
"pushoversounds incoming": "واردة",
"pushoversounds intermission": "استراحة",
"pushoversounds magic": "سحر",
"pushoversounds mechanical": "ميكانيكي",
"pushoversounds pianobar": "شريط البيانو",
"pushoversounds siren": "صفارة إنذار",
"pushoversounds spacealarm": "إنذار الفضاء",
"pushoversounds tugboat": "قارب السحب",
"pushoversounds alien": "إنذار أجنبي (طويل)",
"pushoversounds climb": "تسلق (طويل)",
"pushoversounds persistent": "مستمر (طويل)",
"pushoversounds echo": "صدى مهووس (طويل)",
"pushoversounds updown": "صعودا (طويلة)",
"pushoversounds vibrate": "يهتز فقط",
"pushoversounds none": "لا شيء (صامت)",
pushyAPIKey: "مفتاح API السري",
pushyToken: "رمز الجهاز",
"Show update if available": "عرض التحديث إذا كان ذلك متاحًا",
"Also check beta release": "تحقق أيضًا من الإصدار التجريبي",
"Using a Reverse Proxy?": "باستخدام وكيل عكسي؟",
"Check how to config it for WebSocket": "تحقق من كيفية تكوينه لـ WebSocket",
"Steam Game Server": "خادم لعبة البخار",
"Most likely causes": "الأسباب المرجحة",
"The resource is no longer available.": "لم يعد المورد متاحًا.",
"There might be a typing error in the address.": "قد يكون هناك خطأ مطبعي في العنوان.",
"What you can try": "ماذا تستطيع أن تجرب",
"Retype the address.": "اعد كتابة العنوان.",
"Go back to the previous page.": "عد للصفحة السابقة.",
"Coming Soon": "قريبا",
wayToGetClickSendSMSToken: "يمكنك الحصول على اسم مستخدم API ومفتاح API من {0}.",
"Connection String": "سلسلة الاتصال",
Query: "استفسار",
settingsCertificateExpiry: "شهادة TLS انتهاء الصلاحية",
certificationExpiryDescription: "شاشات HTTPS تضيء عندما تنتهي شهادة TLS في",
"Setup Docker Host": "إعداد مضيف Docker",
"Connection Type": "نوع الاتصال",
"Docker Daemon": "Docker Daemon",
deleteDockerHostMsg: "هل أنت متأكد من حذف مضيف Docker لجميع الشاشات؟",
socket: "قابس كهرباء",
tcp: "TCP / HTTP",
"Docker Container": "حاوية Docker",
"Container Name / ID": "اسم الحاوية / معرف",
"Docker Host": "مضيف Docker",
"Docker Hosts": "مضيفي Docker",
"ntfy Topic": "موضوع ntfy",
Domain: "اِختِصاص",
Workstation: "محطة العمل",
disableCloudflaredNoAuthMsg: "أنت في وضع مصادقة لا توجد كلمة مرور غير مطلوبة.",
trustProxyDescription: "الثقة 'x-forward-*'. إذا كنت ترغب في الحصول على IP العميل الصحيح وكوما في الوقت المناسب مثل Nginx أو Apache ، فيجب عليك تمكين ذلك.",
wayToGetLineNotifyToken: "يمكنك الحصول على رمز الوصول من {0}",
Examples: "أمثلة",
"Home Assistant URL": "Home Assistant URL",
"Long-Lived Access Token": "الرمز المميز للوصول منذ فترة طويلة",
"Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "يمكن إنشاء رمز الوصول منذ فترة طويلة عن طريق النقر على اسم ملف التعريف الخاص بك (أسفل اليسار) والتمرير إلى الأسفل ثم انقر فوق إنشاء الرمز المميز.",
"Notification Service": "خدمة الإخطار",
"default: notify all devices": "الافتراضي: إخطار جميع الأجهزة",
"A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.": "يمكن العثور على قائمة بخدمات الإخطار في المساعد المنزلي ضمن \"Developer Tools > Services\" ابحث عن \"notification\" للعثور على اسم جهازك/هاتفك.",
"Automations can optionally be triggered in Home Assistant": "يمكن أن يتم تشغيل الأتمتة اختياريًا في مساعد المنزل",
"Trigger type": "نوع الزناد",
"Event type": "نوع الحدث",
"Event data": "بيانات الحدث",
"Then choose an action, for example switch the scene to where an RGB light is red.": "ثم اختر إجراءً على سبيل المثال قم بتبديل المشهد إلى حيث يكون ضوء RGB أحمر.",
"Frontend Version": "إصدار الواجهة الأمامية",
"Frontend Version do not match backend version!": "إصدار Frontend لا يتطابق مع الإصدار الخلفي!",
"Base URL": "عنوان URL الأساسي",
goAlertInfo: "الهدف هو تطبيق مفتوح المصدر لجدولة الجدولة التلقائية والإشعارات (مثل الرسائل القصيرة أو المكالمات الصوتية). إشراك الشخص المناسب تلقائيًا بالطريقة الصحيحة وفي الوقت المناسب! {0}",
goAlertIntegrationKeyInfo: "احصل على مفتاح تكامل API العام للخدمة في هذا التنسيق \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" عادةً قيمة المعلمة الرمزية لعنوان url المنسق.",
goAlert: "المرمى",
backupOutdatedWarning: "إهمال",
backupRecommend: "يرجى النسخ الاحتياطي لحجم الصوت أو مجلد البيانات (./data/) مباشرة بدلاً من ذلك.",
Optional: "اختياري",
squadcast: "القاء فريقي",
SendKey: "Sendkey",
"SMSManager API Docs": "مستندات SMSManager API",
"Gateway Type": "نوع البوابة",
SMSManager: "smsmanager",
"You can divide numbers with": "يمكنك تقسيم الأرقام مع",
or: "أو",
recurringInterval: "فترة",
Recurring: "يتكرر",
strategyManual: "نشط/غير نشط يدويًا",
warningTimezone: "إنه يستخدم المنطقة الزمنية للخادم",
weekdayShortMon: "الاثنين",
weekdayShortTue: "الثلاثاء",
weekdayShortWed: "تزوج",
weekdayShortThu: "الخميس",
weekdayShortFri: "الجمعة",
weekdayShortSat: "جلس",
weekdayShortSun: "شمس",
dayOfWeek: "يوم من الأسبوع",
dayOfMonth: "يوم من الشهر",
lastDay: "بالأمس",
lastDay1: "آخر يوم من الشهر",
lastDay2: "الثاني في اليوم الأخير من الشهر",
lastDay3: "الثالث في اليوم الأخير من الشهر",
lastDay4: "الرابع في اليوم الأخير من الشهر",
"No Maintenance": "لا صيانة",
pauseMaintenanceMsg: "هل أنت متأكد من أن تتوقف مؤقتًا؟",
"maintenanceStatus-under-maintenance": "تحت الصيانة",
"maintenanceStatus-inactive": "غير نشط",
"maintenanceStatus-scheduled": "المقرر",
"maintenanceStatus-ended": "انتهى",
"maintenanceStatus-unknown": "مجهول",
"Display Timezone": "عرض المنطقة الزمنية",
"Server Timezone": "المنطقة الزمنية الخادم",
statusPageMaintenanceEndDate: "نهاية",
IconUrl: "url url icon",
"Enable DNS Cache": "تمكين ذاكرة التخزين المؤقت DNS",
Enable: "يُمكَِن",
Disable: "إبطال",
dnsCacheDescription: "قد لا يعمل في بعض بيئات IPv6 تعطيله إذا واجهت أي مشكلات.",
"Single Maintenance Window": "نافذة صيانة واحدة",
"Maintenance Time Window of a Day": "نافذة وقت الصيانة لليوم",
"Effective Date Range": "نطاق التاريخ السريع",
"Schedule Maintenance": "جدولة الصيانة",
"Date and Time": "التاريخ و الوقت",
"DateTime Range": "نطاق DateTime",
Strategy: "إستراتيجية",
"Free Mobile User Identifier": "معرف مستخدم الهاتف المحمول المجاني",
"Free Mobile API Key": "مفتاح واجهة برمجة تطبيقات مجانية للهاتف المحمول",
"Enable TLS": "تمكين TLS",
"Proto Service Name": "اسم خدمة البروتو",
"Proto Method": "طريقة البروتو",
"Proto Content": "محتوى proto",
Economy: "اقتصاد",
Lowcost: "تكلفة منخفضة",
high: "عالي",
"General Monitor Type": "نوع الشاشة العامة",
"Passive Monitor Type": "نوع الشاشة السلبي",
"Specific Monitor Type": "نوع شاشة محدد",
dataRetentionTimeError: "يجب أن تكون فترة الاستبقاء 0 أو أكبر",
infiniteRetention: "ضبط على 0 للاحتفاظ لا نهائي.",
confirmDeleteTagMsg: "هل أنت متأكد من أنك تريد حذف هذه العلامة؟ لن يتم حذف الشاشات المرتبطة بهذه العلامة.",
};

View file

@ -8,12 +8,27 @@ export default {
ignoreTLSError: "Ignorovat TLS/SSL chyby na HTTPS stránkách", ignoreTLSError: "Ignorovat TLS/SSL chyby na HTTPS stránkách",
upsideDownModeDescription: "Pomocí této možnosti změníte způsob vyhodnocování stavu. Pokud je služba dosažitelná, je NEDOSTUPNÁ.", upsideDownModeDescription: "Pomocí této možnosti změníte způsob vyhodnocování stavu. Pokud je služba dosažitelná, je NEDOSTUPNÁ.",
maxRedirectDescription: "Maximální počet přesměrování, která se mají následovat. Nastavením hodnoty 0 zakážete přesměrování.", maxRedirectDescription: "Maximální počet přesměrování, která se mají následovat. Nastavením hodnoty 0 zakážete přesměrování.",
enableGRPCTls: "Umožnit odeslání gRPC žádosti během TLS spojení",
grpcMethodDescription: "Název metody se převede do cammelCase formátu jako je sayHello, check, aj.",
acceptedStatusCodesDescription: "Vyberte stavové kódy, které jsou považovány za úspěšnou odpověď.", acceptedStatusCodesDescription: "Vyberte stavové kódy, které jsou považovány za úspěšnou odpověď.",
Maintenance: "Údržba",
statusMaintenance: "Údržba",
"Schedule maintenance": "Naplánovat údržbu",
"Affected Monitors": "Dotčené dohledy",
"Pick Affected Monitors...": "Vyberte dotčené dohledy…",
"Start of maintenance": "Zahájit údržbu",
"All Status Pages": "Všechny stavové stránky",
"Select status pages...": "Vyberte stavovou stránku…",
recurringIntervalMessage: "Spustit jednou každý den | Spustit jednou každých {0} dní",
affectedMonitorsDescription: "Vyberte dohledy, které budou ovlivněny touto údržbou",
affectedStatusPages: "Zobrazit tuto zprávu o údržbě na vybraných stavových stránkách",
atLeastOneMonitor: "Vyberte alespoň jeden dotčený dohled",
passwordNotMatchMsg: "Hesla se neshodují", passwordNotMatchMsg: "Hesla se neshodují",
notificationDescription: "Pro zajištění funkčnosti oznámení je nutné jej přiřadit dohledu.", notificationDescription: "Pro zajištění funkčnosti oznámení je nutné jej přiřadit dohledu.",
keywordDescription: "Vyhledat klíčové slovo v prosté odpovědi HTML nebo JSON. Při hledání se rozlišuje velikost písmen.", keywordDescription: "Vyhledat klíčové slovo v prosté odpovědi HTML nebo JSON. Při hledání se rozlišuje velikost písmen.",
pauseDashboardHome: "Pozastavit", pauseDashboardHome: "Pozastaveno",
deleteMonitorMsg: "Opravdu chcete odstranit tento dohled?", deleteMonitorMsg: "Opravdu chcete odstranit tento dohled?",
deleteMaintenanceMsg: "Opravdu chcete odstranit tuto údržbu?",
deleteNotificationMsg: "Opravdu chcete odstranit toto oznámení pro všechny dohledy?", deleteNotificationMsg: "Opravdu chcete odstranit toto oznámení pro všechny dohledy?",
dnsPortDescription: "Port DNS serveru. Standardně běží na portu 53. V případě potřeby jej můžete kdykoli změnit.", dnsPortDescription: "Port DNS serveru. Standardně běží na portu 53. V případě potřeby jej můžete kdykoli změnit.",
resolverserverDescription: "Cloudflare je výchozí server. V případě potřeby můžete Resolver server kdykoli změnit.", resolverserverDescription: "Cloudflare je výchozí server. V případě potřeby můžete Resolver server kdykoli změnit.",
@ -47,7 +62,7 @@ export default {
Down: "Nedostupný", Down: "Nedostupný",
Pending: "Čekám", Pending: "Čekám",
Unknown: "Neznámý", Unknown: "Neznámý",
Pause: "Pozastaveno", Pause: "Pozastavit",
Name: "Název", Name: "Název",
Status: "Stav", Status: "Stav",
DateTime: "Časové razítko", DateTime: "Časové razítko",
@ -59,6 +74,7 @@ export default {
Current: "Aktuální", Current: "Aktuální",
Uptime: "Doba provozu", Uptime: "Doba provozu",
"Cert Exp.": "Platnost certifikátu", "Cert Exp.": "Platnost certifikátu",
Monitor: "Dohled | Dohledy",
day: "den | dny/í", day: "den | dny/í",
"-day": "-dní", "-day": "-dní",
hour: "hodina", hour: "hodina",
@ -175,6 +191,7 @@ export default {
Indigo: "Indigo", Indigo: "Indigo",
Purple: "Purpurová", Purple: "Purpurová",
Pink: "Růžová", Pink: "Růžová",
Custom: "Vlastní",
"Search...": "Hledat…", "Search...": "Hledat…",
"Avg. Ping": "Průměr Ping", "Avg. Ping": "Průměr Ping",
"Avg. Response": "Průměr Odpověď", "Avg. Response": "Průměr Odpověď",
@ -194,6 +211,7 @@ export default {
here: "sem", here: "sem",
Required: "Vyžadováno", Required: "Vyžadováno",
telegram: "Telegram", telegram: "Telegram",
"ZohoCliq": "ZohoCliq",
"Bot Token": "Token robota", "Bot Token": "Token robota",
wayToGetTelegramToken: "Token můžete získat od {0}.", wayToGetTelegramToken: "Token můžete získat od {0}.",
"Chat ID": "ID chatu", "Chat ID": "ID chatu",
@ -206,6 +224,8 @@ export default {
"Content Type": "Typ obsahu", "Content Type": "Typ obsahu",
webhookJsonDesc: "{0} je vhodný pro všechny moderní servery HTTP, jako je Express.js", webhookJsonDesc: "{0} je vhodný pro všechny moderní servery HTTP, jako je Express.js",
webhookFormDataDesc: "{multipart} je vhodné pro PHP. JSON bude nutné analyzovat prostřednictvím {decodeFunction}", webhookFormDataDesc: "{multipart} je vhodné pro PHP. JSON bude nutné analyzovat prostřednictvím {decodeFunction}",
webhookAdditionalHeadersTitle: "Dodatečné hlavičky",
webhookAdditionalHeadersDesc: "Nastavte dodatečné hlavičky, které se odešlou společně s webhookem.",
smtp: "E-mail (SMTP)", smtp: "E-mail (SMTP)",
secureOptionNone: "Žádné / STARTTLS (25, 587)", secureOptionNone: "Žádné / STARTTLS (25, 587)",
secureOptionTLS: "TLS (465)", secureOptionTLS: "TLS (465)",
@ -223,7 +243,8 @@ export default {
"Hello @everyone is...": "Dobrý den, {'@'}všichni jsou…", "Hello @everyone is...": "Dobrý den, {'@'}všichni jsou…",
teams: "Microsoft Teams", teams: "Microsoft Teams",
"Webhook URL": "URL adresa webhooku", "Webhook URL": "URL adresa webhooku",
wayToGetTeamsURL: "Informace o tom, jak vytvořit URL adresu webhooku naleznete {0}.", wayToGetTeamsURL: "Informace o tom, jak vytvořit URL adresu webhooku naleznete na {0}.",
wayToGetZohoCliqURL: "Informace o tom, jak vytvořit URL adresu webhooku naleznete na {0}.",
signal: "Signal", signal: "Signal",
Number: "Číslo", Number: "Číslo",
Recipients: "Příjemci", Recipients: "Příjemci",
@ -253,6 +274,10 @@ export default {
apprise: "Apprise (podpora více než 50 oznamovacích služeb)", apprise: "Apprise (podpora více než 50 oznamovacích služeb)",
GoogleChat: "Google Chat (pouze Google Workspace)", GoogleChat: "Google Chat (pouze Google Workspace)",
pushbullet: "Pushbullet", pushbullet: "Pushbullet",
Kook: "Kook",
wayToGetKookBotToken: "Aplikaci vytvoříte a token bota získáte na {0}",
wayToGetKookGuildID: "V nastavení Kook aktivujte 'Vývojářský režim' a kliknutím pravým tlačítkem na guild získejte jeho ID",
"Guild ID": "Guild ID",
line: "Line Messenger", line: "Line Messenger",
mattermost: "Mattermost", mattermost: "Mattermost",
"User Key": "Klíč uživatele", "User Key": "Klíč uživatele",
@ -297,6 +322,7 @@ export default {
promosmsTypeSpeed: "SMS SPEED nejvyšší priorita v systému. Velmi rychlé a spolehlivé, ale nákladné (přibližně dvojnásobek ceny SMS FULL).", promosmsTypeSpeed: "SMS SPEED nejvyšší priorita v systému. Velmi rychlé a spolehlivé, ale nákladné (přibližně dvojnásobek ceny SMS FULL).",
promosmsPhoneNumber: "Telefonní číslo (polští příjemci mohou vynechat telefonní předvolbu)", promosmsPhoneNumber: "Telefonní číslo (polští příjemci mohou vynechat telefonní předvolbu)",
promosmsSMSSender: "Odesílatel SMS: Předem zaregistrovaný název nebo jeden z výchozích: InfoSMS, SMS Info, MaxSMS, INFO, SMS", promosmsSMSSender: "Odesílatel SMS: Předem zaregistrovaný název nebo jeden z výchozích: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
promosmsAllowLongSMS: "Povolit dlouhé SMS",
"Feishu WebHookUrl": "Feishu WebHookURL", "Feishu WebHookUrl": "Feishu WebHookURL",
matrixHomeserverURL: "URL adresa domácího serveru (s http(s):// a volitelně portem)", matrixHomeserverURL: "URL adresa domácího serveru (s http(s):// a volitelně portem)",
"Internal Room Id": "ID interní místnosti", "Internal Room Id": "ID interní místnosti",
@ -365,6 +391,16 @@ export default {
serwersmsAPIPassword: "API heslo", serwersmsAPIPassword: "API heslo",
serwersmsPhoneNumber: "Telefonní číslo", serwersmsPhoneNumber: "Telefonní číslo",
serwersmsSenderName: "Odesílatel SMS (registrováno prostřednictvím zákaznického portálu)", serwersmsSenderName: "Odesílatel SMS (registrováno prostřednictvím zákaznického portálu)",
smseagle: "SMSEagle",
smseagleTo: "Telefonní číslo(a)",
smseagleGroup: "Název skupiny v adresáři",
smseagleContact: "Název kontaktu v adresáři",
smseagleRecipientType: "Typ příjemce",
smseagleRecipient: "Příjemce(i) (více záznamů oddělte čárkou)",
smseagleToken: "API přístupový token",
smseagleUrl: "URL vašeho SMSEagle zařízení",
smseagleEncoding: "Odeslat v Unicode",
smseaglePriority: "Priorita zprávy (0-9, výchozí = 0)",
"stackfield": "Stackfield", "stackfield": "Stackfield",
Customize: "Přizpůsobit", Customize: "Přizpůsobit",
"Custom Footer": "Vlastní patička", "Custom Footer": "Vlastní patička",
@ -588,11 +624,11 @@ export default {
"SMSManager API Docs": "SMSManager API Docs ", "SMSManager API Docs": "SMSManager API Docs ",
"Gateway Type": "Gateway Typ", "Gateway Type": "Gateway Typ",
SMSManager: "SMSManager", SMSManager: "SMSManager",
"You can divide numbers with": "Čísla můžete dělit pomocí", "You can divide numbers with": "Čísla můžete oddělit pomocí",
"or": "nebo", "or": "nebo",
recurringInterval: "Interval", recurringInterval: "Interval",
"Recurring": "Opakující se", "Recurring": "Opakující se",
strategyManual: "Aktivní/Neaktivní Ručně", strategyManual: "Ruční spuštění/vypnutí",
warningTimezone: "Používá se časové pásmo serveru", warningTimezone: "Používá se časové pásmo serveru",
weekdayShortMon: "Po", weekdayShortMon: "Po",
weekdayShortTue: "Út", weekdayShortTue: "Út",
@ -608,8 +644,8 @@ export default {
lastDay2: "2. poslední den v měsíci", lastDay2: "2. poslední den v měsíci",
lastDay3: "3. poslední den v měsíci", lastDay3: "3. poslední den v měsíci",
lastDay4: "4. poslední den v měsíci", lastDay4: "4. poslední den v měsíci",
"No Maintenance": "Žádna údržba", "No Maintenance": "Žádná údržba",
pauseMaintenanceMsg: "Jsi si jistý, že chceš pozastavit údržbu?", pauseMaintenanceMsg: "Opravdu chcete pozastavit údržbu?",
"maintenanceStatus-under-maintenance": "Údržba", "maintenanceStatus-under-maintenance": "Údržba",
"maintenanceStatus-inactive": "Neaktivní", "maintenanceStatus-inactive": "Neaktivní",
"maintenanceStatus-scheduled": "Naplánováno", "maintenanceStatus-scheduled": "Naplánováno",
@ -622,5 +658,27 @@ export default {
"Enable DNS Cache": "Povolit DNS Cache", "Enable DNS Cache": "Povolit DNS Cache",
"Enable": "Povolit", "Enable": "Povolit",
"Disable": "Zakázat", "Disable": "Zakázat",
dnsCacheDescription: "V některých prostředích IPv6 nemusí fungovat. Pokud narazíte na nějaké problémy, vypněte jej.", dnsCacheDescription: "V některých IPv6 prostředích nemusí fungovat. Pokud narazíte na nějaké problémy, vypněte jej.",
"Single Maintenance Window": "Konkrétní časové okno pro údržbu",
"Maintenance Time Window of a Day": "Časové okno pro údržbu v daný den",
"Effective Date Range": "Časové období",
"Schedule Maintenance": "Naplánovat údržbu",
"Date and Time": "Datum a čas",
"DateTime Range": "Rozsah data a času",
Strategy: "Strategie",
"Free Mobile User Identifier": "Free Mobile User Identifier",
"Free Mobile API Key": "Free Mobile API Key",
"Enable TLS": "Povolit TLS",
"Proto Service Name": "Proto Service Name",
"Proto Method": "Proto Method",
"Proto Content": "Proto Content",
Economy: "Úsporná",
Lowcost: "Nízkonákladová",
high: "high",
"General Monitor Type": "Obecný typ dohledu",
"Passive Monitor Type": "Pasivní typ dohledu",
"Specific Monitor Type": "Konkrétní typ dohledu",
dataRetentionTimeError: "Doba pro uchování musí být větší nebo rovna 0",
infiniteRetention: "Pro nekonečný záznam zadejte 0.",
confirmDeleteTagMsg: "Opravdu chcete odstranit tento štíte? Provedením této akce nedojde k odstranění dohledů, které jej mají přiřazeny.",
}; };

View file

@ -181,7 +181,7 @@ export default {
"Add New below or Select...": "Add New below or Select...", "Add New below or Select...": "Add New below or Select...",
"Tag with this name already exist.": "Tag with this name already exists.", "Tag with this name already exist.": "Tag with this name already exists.",
"Tag with this value already exist.": "Tag with this value already exists.", "Tag with this value already exist.": "Tag with this value already exists.",
color: "color", color: "Color",
"value (optional)": "value (optional)", "value (optional)": "value (optional)",
Gray: "Gray", Gray: "Gray",
Red: "Red", Red: "Red",
@ -322,6 +322,7 @@ export default {
promosmsTypeSpeed: "SMS SPEED - Highest priority in system. Very quick and reliable but costly (about twice of SMS FULL price).", promosmsTypeSpeed: "SMS SPEED - Highest priority in system. Very quick and reliable but costly (about twice of SMS FULL price).",
promosmsPhoneNumber: "Phone number (for Polish recipient You can skip area codes)", promosmsPhoneNumber: "Phone number (for Polish recipient You can skip area codes)",
promosmsSMSSender: "SMS Sender Name : Pre-registred name or one of defaults: InfoSMS, SMS Info, MaxSMS, INFO, SMS", promosmsSMSSender: "SMS Sender Name : Pre-registred name or one of defaults: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
promosmsAllowLongSMS: "Allow long SMS",
"Feishu WebHookUrl": "Feishu WebHookURL", "Feishu WebHookUrl": "Feishu WebHookURL",
matrixHomeserverURL: "Homeserver URL (with http(s):// and optionally port)", matrixHomeserverURL: "Homeserver URL (with http(s):// and optionally port)",
"Internal Room Id": "Internal Room ID", "Internal Room Id": "Internal Room ID",

View file

@ -209,7 +209,7 @@ export default {
here: "ici", here: "ici",
Required: "Requis", Required: "Requis",
telegram: "Telegram", telegram: "Telegram",
"ZohoCliq": "ZohoCliq", ZohoCliq: "ZohoCliq",
"Bot Token": "Jeton du robot", "Bot Token": "Jeton du robot",
wayToGetTelegramToken: "Vous pouvez obtenir un token depuis {0}.", wayToGetTelegramToken: "Vous pouvez obtenir un token depuis {0}.",
"Chat ID": "Chat ID", "Chat ID": "Chat ID",
@ -308,7 +308,7 @@ export default {
lineDevConsoleTo: "Console développeurs Line - {0}", lineDevConsoleTo: "Console développeurs Line - {0}",
"Basic Settings": "Paramètres de base", "Basic Settings": "Paramètres de base",
"User ID": "Identifiant utilisateur", "User ID": "Identifiant utilisateur",
"Messaging API": "Messaging API", // Ne pas traduire, il s'agit du type de salon affiché sur la console développeurs Line "Messaging API": "Messaging API",
wayToGetLineChannelToken: "Premièrement accédez à {0}, créez un <i>provider</i> et définissez un type de salon à « Messaging API ». Vous pourrez alors avoir puis vous pourrez avoir le jeton d'accès du salon et l'identifiant utilisateur demandés.", wayToGetLineChannelToken: "Premièrement accédez à {0}, créez un <i>provider</i> et définissez un type de salon à « Messaging API ». Vous pourrez alors avoir puis vous pourrez avoir le jeton d'accès du salon et l'identifiant utilisateur demandés.",
"Icon URL": "URL vers l'icône", "Icon URL": "URL vers l'icône",
aboutIconURL: "Vous pouvez mettre un lien vers une image dans « URL vers l'icône » pour remplacer l'image de profil par défaut. Elle ne sera utilisé que si « Icône émoji » n'est pas défini.", aboutIconURL: "Vous pouvez mettre un lien vers une image dans « URL vers l'icône » pour remplacer l'image de profil par défaut. Elle ne sera utilisé que si « Icône émoji » n'est pas défini.",
@ -669,12 +669,16 @@ export default {
"Proto Service Name": "Nom du service proto", "Proto Service Name": "Nom du service proto",
"Proto Method": "Méthode Proto", "Proto Method": "Méthode Proto",
"Proto Content": "Contenu proto", "Proto Content": "Contenu proto",
"Economy": "Économique", Economy: "Économique",
"Lowcost": "Faible coût", Lowcost: "Faible coût",
"high": "Haute", high: "Haute",
"General Monitor Type": "Type de sonde générale", "General Monitor Type": "Type de sonde générale",
"Passive Monitor Type": "Type de sonde passive", "Passive Monitor Type": "Type de sonde passive",
"Specific Monitor Type": "Type de sonde spécifique", "Specific Monitor Type": "Type de sonde spécifique",
dataRetentionTimeError: "La durée de conservation doit être supérieure ou égale à 0", dataRetentionTimeError: "La durée de conservation doit être supérieure ou égale à 0",
infiniteRetention: "Définissez la valeur à 0 pour une durée de conservation infinie.", infiniteRetention: "Définissez la valeur à 0 pour une durée de conservation infinie.",
Monitor: "Sonde | Sondes",
Custom: "Personnalisé",
confirmDeleteTagMsg: "Voulez-vous vraiment supprimer cette étiquettes ? Les moniteurs associés ne seront pas supprimés.",
promosmsAllowLongSMS: "Autoriser les longs SMS",
}; };

View file

@ -284,6 +284,7 @@ export default {
promosmsTypeSpeed: "SMS SPEED - wysyłka priorytetowa, ma wszystkie zalety SMS FULL", promosmsTypeSpeed: "SMS SPEED - wysyłka priorytetowa, ma wszystkie zalety SMS FULL",
promosmsPhoneNumber: "Numer odbiorcy", promosmsPhoneNumber: "Numer odbiorcy",
promosmsSMSSender: "Nadawca SMS (wcześniej zatwierdzone nazwy z panelu PromoSMS)", promosmsSMSSender: "Nadawca SMS (wcześniej zatwierdzone nazwy z panelu PromoSMS)",
promosmsAllowLongSMS: "Zezwól na długie SMSy",
"Primary Base URL": "Główny URL", "Primary Base URL": "Główny URL",
"Push URL": "Push URL", "Push URL": "Push URL",
needPushEvery: "Powinieneś wywoływać ten URL co {0} sekund", needPushEvery: "Powinieneś wywoływać ten URL co {0} sekund",

View file

@ -63,6 +63,12 @@
</router-link> </router-link>
</li> </li>
<li>
<a href="https://github.com/louislam/uptime-kuma/wiki" class="dropdown-item" target="_blank">
<font-awesome-icon icon="info-circle" /> {{ $t("Help") }}
</a>
</li>
<li v-if="$root.loggedIn && $root.socket.token !== 'autoLogin'"> <li v-if="$root.loggedIn && $root.socket.token !== 'autoLogin'">
<button class="dropdown-item" @click="$root.logout"> <button class="dropdown-item" @click="$root.logout">
<font-awesome-icon icon="sign-out-alt" /> <font-awesome-icon icon="sign-out-alt" />

View file

@ -12,6 +12,11 @@ export default {
}, },
methods: { methods: {
/**
* Convert value to UTC
* @param {string | number | Date | dayjs.Dayjs} value
* @returns {dayjs.Dayjs}
*/
toUTC(value) { toUTC(value) {
return dayjs.tz(value, this.timezone).utc().format(); return dayjs.tz(value, this.timezone).utc().format();
}, },
@ -34,6 +39,11 @@ export default {
return this.datetimeFormat(value, "YYYY-MM-DD HH:mm:ss"); return this.datetimeFormat(value, "YYYY-MM-DD HH:mm:ss");
}, },
/**
* Get time for maintenance
* @param {string | number | Date | dayjs.Dayjs} value
* @returns {string}
*/
datetimeMaintenance(value) { datetimeMaintenance(value) {
const inputDate = new Date(value); const inputDate = new Date(value);
const now = new Date(Date.now()); const now = new Date(Date.now());

View file

@ -3,6 +3,7 @@ import { useToast } from "vue-toastification";
import jwtDecode from "jwt-decode"; import jwtDecode from "jwt-decode";
import Favico from "favico.js"; import Favico from "favico.js";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { DOWN, MAINTENANCE, PENDING, UP } from "../util.ts";
const toast = useToast(); const toast = useToast();
let socket; let socket;
@ -454,6 +455,10 @@ export default {
socket.emit("getMonitorList", callback); socket.emit("getMonitorList", callback);
}, },
/**
* Get list of maintenances
* @param {socketCB} callback
*/
getMaintenanceList(callback) { getMaintenanceList(callback) {
if (! callback) { if (! callback) {
callback = () => { }; callback = () => { };
@ -470,22 +475,49 @@ export default {
socket.emit("add", monitor, callback); socket.emit("add", monitor, callback);
}, },
/**
* Adds a maintenace
* @param {Object} maintenance
* @param {socketCB} callback
*/
addMaintenance(maintenance, callback) { addMaintenance(maintenance, callback) {
socket.emit("addMaintenance", maintenance, callback); socket.emit("addMaintenance", maintenance, callback);
}, },
/**
* Add monitors to maintenance
* @param {number} maintenanceID
* @param {number[]} monitors
* @param {socketCB} callback
*/
addMonitorMaintenance(maintenanceID, monitors, callback) { addMonitorMaintenance(maintenanceID, monitors, callback) {
socket.emit("addMonitorMaintenance", maintenanceID, monitors, callback); socket.emit("addMonitorMaintenance", maintenanceID, monitors, callback);
}, },
/**
* Add status page to maintenance
* @param {number} maintenanceID
* @param {number} statusPages
* @param {socketCB} callback
*/
addMaintenanceStatusPage(maintenanceID, statusPages, callback) { addMaintenanceStatusPage(maintenanceID, statusPages, callback) {
socket.emit("addMaintenanceStatusPage", maintenanceID, statusPages, callback); socket.emit("addMaintenanceStatusPage", maintenanceID, statusPages, callback);
}, },
/**
* Get monitors affected by maintenance
* @param {number} maintenanceID
* @param {socketCB} callback
*/
getMonitorMaintenance(maintenanceID, callback) { getMonitorMaintenance(maintenanceID, callback) {
socket.emit("getMonitorMaintenance", maintenanceID, callback); socket.emit("getMonitorMaintenance", maintenanceID, callback);
}, },
/**
* Get status pages where maintenance is shown
* @param {number} maintenanceID
* @param {socketCB} callback
*/
getMaintenanceStatusPage(maintenanceID, callback) { getMaintenanceStatusPage(maintenanceID, callback) {
socket.emit("getMaintenanceStatusPage", maintenanceID, callback); socket.emit("getMaintenanceStatusPage", maintenanceID, callback);
}, },
@ -499,6 +531,11 @@ export default {
socket.emit("deleteMonitor", monitorID, callback); socket.emit("deleteMonitor", monitorID, callback);
}, },
/**
* Delete specified maintenance
* @param {number} maintenanceID
* @param {socketCB} callback
*/
deleteMaintenance(maintenanceID, callback) { deleteMaintenance(maintenanceID, callback) {
socket.emit("deleteMaintenance", maintenanceID, callback); socket.emit("deleteMaintenance", maintenanceID, callback);
}, },
@ -590,28 +627,28 @@ export default {
for (let monitorID in this.lastHeartbeatList) { for (let monitorID in this.lastHeartbeatList) {
let lastHeartBeat = this.lastHeartbeatList[monitorID]; let lastHeartBeat = this.lastHeartbeatList[monitorID];
if (this.monitorList[monitorID] && this.monitorList[monitorID].maintenance) { if (! lastHeartBeat) {
result[monitorID] = {
text: this.$t("statusMaintenance"),
color: "maintenance",
};
} else if (! lastHeartBeat) {
result[monitorID] = unknown; result[monitorID] = unknown;
} else if (lastHeartBeat.status === 1) { } else if (lastHeartBeat.status === UP) {
result[monitorID] = { result[monitorID] = {
text: this.$t("Up"), text: this.$t("Up"),
color: "primary", color: "primary",
}; };
} else if (lastHeartBeat.status === 0) { } else if (lastHeartBeat.status === DOWN) {
result[monitorID] = { result[monitorID] = {
text: this.$t("Down"), text: this.$t("Down"),
color: "danger", color: "danger",
}; };
} else if (lastHeartBeat.status === 2) { } else if (lastHeartBeat.status === PENDING) {
result[monitorID] = { result[monitorID] = {
text: this.$t("Pending"), text: this.$t("Pending"),
color: "warning", color: "warning",
}; };
} else if (lastHeartBeat.status === MAINTENANCE) {
result[monitorID] = {
text: this.$t("statusMaintenance"),
color: "maintenance",
};
} else { } else {
result[monitorID] = unknown; result[monitorID] = unknown;
} }
@ -633,17 +670,17 @@ export default {
let beat = this.$root.lastHeartbeatList[monitorID]; let beat = this.$root.lastHeartbeatList[monitorID];
let monitor = this.$root.monitorList[monitorID]; let monitor = this.$root.monitorList[monitorID];
if (monitor && monitor.maintenance) { if (monitor && ! monitor.active) {
result.maintenance++;
} else if (monitor && ! monitor.active) {
result.pause++; result.pause++;
} else if (beat) { } else if (beat) {
if (beat.status === 1) { if (beat.status === UP) {
result.up++; result.up++;
} else if (beat.status === 0) { } else if (beat.status === DOWN) {
result.down++; result.down++;
} else if (beat.status === 2) { } else if (beat.status === PENDING) {
result.up++; result.up++;
} else if (beat.status === MAINTENANCE) {
result.maintenance++;
} else { } else {
result.unknown++; result.unknown++;
} }

View file

@ -356,6 +356,7 @@ export default {
}); });
}, },
methods: { methods: {
/** Initialise page */
init() { init() {
this.affectedMonitors = []; this.affectedMonitors = [];
this.selectedStatusPages = []; this.selectedStatusPages = [];
@ -414,6 +415,7 @@ export default {
} }
}, },
/** Create new maintenance */
async submit() { async submit() {
this.processing = true; this.processing = true;
@ -458,6 +460,11 @@ export default {
} }
}, },
/**
* Add monitor to maintenance
* @param {number} maintenanceID
* @param {socketCB} callback
*/
async addMonitorMaintenance(maintenanceID, callback) { async addMonitorMaintenance(maintenanceID, callback) {
await this.$root.addMonitorMaintenance(maintenanceID, this.affectedMonitors, async (res) => { await this.$root.addMonitorMaintenance(maintenanceID, this.affectedMonitors, async (res) => {
if (!res.ok) { if (!res.ok) {
@ -470,6 +477,11 @@ export default {
}); });
}, },
/**
* Add status page to maintenance
* @param {number} maintenanceID
* @param {socketCB} callback
*/
async addMaintenanceStatusPage(maintenanceID, callback) { async addMaintenanceStatusPage(maintenanceID, callback) {
await this.$root.addMaintenanceStatusPage(maintenanceID, (this.showOnAllPages) ? this.selectedStatusPagesOptions : this.selectedStatusPages, async (res) => { await this.$root.addMaintenanceStatusPage(maintenanceID, (this.showOnAllPages) ? this.selectedStatusPagesOptions : this.selectedStatusPages, async (res) => {
if (!res.ok) { if (!res.ok) {

View file

@ -111,7 +111,7 @@
<!-- TCP Port / Ping / DNS / Steam / MQTT / Radius only --> <!-- TCP Port / Ping / DNS / Steam / MQTT / Radius only -->
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'mqtt' || monitor.type === 'radius'" class="my-3"> <div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'mqtt' || monitor.type === 'radius'" class="my-3">
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label> <label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
<input id="hostname" v-model="monitor.hostname" type="text" class="form-control" :pattern="`${ipRegexPattern}|${hostnameRegexPattern}`" required> <input id="hostname" v-model="monitor.hostname" type="text" class="form-control" :pattern="`${monitor.type === 'mqtt' ? mqttIpOrHostnameRegexPattern : ipOrHostnameRegexPattern}`" required>
</div> </div>
<!-- Port --> <!-- Port -->
@ -600,6 +600,7 @@ import DockerHostDialog from "../components/DockerHostDialog.vue";
import ProxyDialog from "../components/ProxyDialog.vue"; import ProxyDialog from "../components/ProxyDialog.vue";
import TagsManager from "../components/TagsManager.vue"; import TagsManager from "../components/TagsManager.vue";
import { genSecret, isDev, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND } from "../util.ts"; import { genSecret, isDev, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND } from "../util.ts";
import { hostNameRegexPattern } from "../util-frontend";
const toast = useToast(); const toast = useToast();
@ -624,11 +625,8 @@ export default {
}, },
acceptedStatusCodeOptions: [], acceptedStatusCodeOptions: [],
dnsresolvetypeOptions: [], dnsresolvetypeOptions: [],
ipOrHostnameRegexPattern: hostNameRegexPattern(),
// Source: https://digitalfortress.tech/tips/top-15-commonly-used-regex/ mqttIpOrHostnameRegexPattern: hostNameRegexPattern(true)
ipRegexPattern: "((^\\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\\s*$)|(^\\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?\\s*$))",
// Source: https://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address
hostnameRegexPattern: "^(([a-zA-Z0-9_]|[a-zA-Z0-9_][a-zA-Z0-9\\-_]*[a-zA-Z0-9_])\\.)*([A-Za-z0-9_]|[A-Za-z0-9_][A-Za-z0-9\\-_]*[A-Za-z0-9_])$"
}; };
}, },

View file

@ -65,6 +65,7 @@ export default {
this.init(); this.init();
}, },
methods: { methods: {
/** Initialise page */
init() { init() {
this.$root.getSocket().emit("getMonitorMaintenance", this.$route.params.id, (res) => { this.$root.getSocket().emit("getMonitorMaintenance", this.$route.params.id, (res) => {
if (res.ok) { if (res.ok) {
@ -83,10 +84,12 @@ export default {
}); });
}, },
/** Confirm deletion */
deleteDialog() { deleteDialog() {
this.$refs.confirmDelete.show(); this.$refs.confirmDelete.show();
}, },
/** Delete maintenance after showing confirmation */
deleteMaintenance() { deleteMaintenance() {
this.$root.deleteMaintenance(this.maintenance.id, (res) => { this.$root.deleteMaintenance(this.maintenance.id, (res) => {
if (res.ok) { if (res.ok) {

View file

@ -133,15 +133,25 @@ export default {
} }
}, },
/**
* Get maintenance URL
* @param {number} id
* @returns {string} Relative URL
*/
maintenanceURL(id) { maintenanceURL(id) {
return getMaintenanceRelativeURL(id); return getMaintenanceRelativeURL(id);
}, },
/**
* Show delete confirmation
* @param {number} maintenanceID
*/
deleteDialog(maintenanceID) { deleteDialog(maintenanceID) {
this.selectedMaintenanceID = maintenanceID; this.selectedMaintenanceID = maintenanceID;
this.$refs.confirmDelete.show(); this.$refs.confirmDelete.show();
}, },
/** Delete maintenance after showing confirmation dialog */
deleteMaintenance() { deleteMaintenance() {
this.$root.deleteMaintenance(this.selectedMaintenanceID, (res) => { this.$root.deleteMaintenance(this.selectedMaintenanceID, (res) => {
if (res.ok) { if (res.ok) {

View file

@ -79,6 +79,22 @@ export function getResBaseURL() {
} }
} }
/**
*
* @param {} mqtt wheather or not the regex should take into account the fact that it is an mqtt uri
* @returns RegExp The requested regex
*/
export function hostNameRegexPattern(mqtt = false) {
// mqtt, mqtts, ws and wss schemes accepted by mqtt.js (https://github.com/mqttjs/MQTT.js/#connect)
const mqttSchemeRegexPattern = "((mqtt|ws)s?:\\/\\/)?";
// Source: https://digitalfortress.tech/tips/top-15-commonly-used-regex/
const ipRegexPattern = `((^\\s*${mqtt ? mqttSchemeRegexPattern : ""}((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\\s*$)|(^\\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?\\s*$))`;
// Source: https://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address
const hostNameRegexPattern = `^${mqtt ? mqttSchemeRegexPattern : ""}([a-zA-Z0-9])?(([a-zA-Z0-9_]|[a-zA-Z0-9_][a-zA-Z0-9\\-_]*[a-zA-Z0-9_])\\.)*([A-Za-z0-9_]|[A-Za-z0-9_][A-Za-z0-9\\-_]*[A-Za-z0-9_])$`;
return `${ipRegexPattern}|${hostNameRegexPattern}`;
}
/** /**
* Get the tag color options * Get the tag color options
* Shared between components * Shared between components

View file

@ -315,6 +315,11 @@ function getMonitorRelativeURL(id) {
return "/dashboard/" + id; return "/dashboard/" + id;
} }
exports.getMonitorRelativeURL = getMonitorRelativeURL; exports.getMonitorRelativeURL = getMonitorRelativeURL;
/**
* Get relative path for maintenance
* @param id ID of maintenance
* @returns Formatted relative path
*/
function getMaintenanceRelativeURL(id) { function getMaintenanceRelativeURL(id) {
return "/maintenance/" + id; return "/maintenance/" + id;
} }
@ -361,6 +366,11 @@ function parseTimeFromTimeObject(obj) {
return result; return result;
} }
exports.parseTimeFromTimeObject = parseTimeFromTimeObject; exports.parseTimeFromTimeObject = parseTimeFromTimeObject;
/**
* Convert ISO date to UTC
* @param input Date
* @returns ISO Date time
*/
function isoToUTCDateTime(input) { function isoToUTCDateTime(input) {
return dayjs(input).utc().format(exports.SQL_DATETIME_FORMAT); return dayjs(input).utc().format(exports.SQL_DATETIME_FORMAT);
} }
@ -379,6 +389,12 @@ function utcToLocal(input, format = exports.SQL_DATETIME_FORMAT) {
return dayjs.utc(input).local().format(format); return dayjs.utc(input).local().format(format);
} }
exports.utcToLocal = utcToLocal; exports.utcToLocal = utcToLocal;
/**
* Convert local datetime to UTC
* @param input Local date
* @param format Format to return
* @returns Date in requested format
*/
function localToUTC(input, format = exports.SQL_DATETIME_FORMAT) { function localToUTC(input, format = exports.SQL_DATETIME_FORMAT) {
return dayjs(input).utc().format(format); return dayjs(input).utc().format(format);
} }

View file

@ -352,6 +352,11 @@ export function getMonitorRelativeURL(id: string) {
return "/dashboard/" + id; return "/dashboard/" + id;
} }
/**
* Get relative path for maintenance
* @param id ID of maintenance
* @returns Formatted relative path
*/
export function getMaintenanceRelativeURL(id: string) { export function getMaintenanceRelativeURL(id: string) {
return "/maintenance/" + id; return "/maintenance/" + id;
} }
@ -405,7 +410,11 @@ export function parseTimeFromTimeObject(obj : any) {
return result; return result;
} }
/**
* Convert ISO date to UTC
* @param input Date
* @returns ISO Date time
*/
export function isoToUTCDateTime(input : string) { export function isoToUTCDateTime(input : string) {
return dayjs(input).utc().format(SQL_DATETIME_FORMAT); return dayjs(input).utc().format(SQL_DATETIME_FORMAT);
} }
@ -424,6 +433,12 @@ export function utcToLocal(input : string, format = SQL_DATETIME_FORMAT) {
return dayjs.utc(input).local().format(format); return dayjs.utc(input).local().format(format);
} }
/**
* Convert local datetime to UTC
* @param input Local date
* @param format Format to return
* @returns Date in requested format
*/
export function localToUTC(input : string, format = SQL_DATETIME_FORMAT) { export function localToUTC(input : string, format = SQL_DATETIME_FORMAT) {
return dayjs(input).utc().format(format); return dayjs(input).utc().format(format);
} }

View file

@ -0,0 +1,33 @@
import { hostNameRegexPattern } from "../../../src/util-frontend";
describe("Test util-frontend.js", () => {
describe("hostNameRegexPattern()", () => {
it('should return a valid regex for non mqtt hostnames', () => {
const regex = new RegExp(hostNameRegexPattern(false));
expect(regex.test("www.test.com")).to.be.true;
expect(regex.test("127.0.0.1")).to.be.true;
expect(regex.test("192.168.1.156")).to.be.true;
["mqtt", "mqtts", "ws", "wss"].forEach(schema => {
expect(regex.test(`${schema}://www.test.com`)).to.be.false;
expect(regex.test(`${schema}://127.0.0.1`)).to.be.false;
});
});
it('should return a valid regex for mqtt hostnames', () => {
const hostnameString = hostNameRegexPattern(false);
console.log('*********', hostnameString, '***********');
const regex = new RegExp(hostNameRegexPattern(true));
expect(regex.test("www.test.com")).to.be.true;
expect(regex.test("127.0.0.1")).to.be.true;
expect(regex.test("192.168.1.156")).to.be.true;
["mqtt", "mqtts", "ws", "wss"].forEach(schema => {
expect(regex.test(`${schema}://www.test.com`)).to.be.true;
expect(regex.test(`${schema}://127.0.0.1`)).to.be.true;
});
});
});
});