Merge branch 'master' into feat-add-elasticsearch

This commit is contained in:
Muhammed Hussein karimi 2023-06-27 18:48:58 +03:30 committed by GitHub
commit 7b705219de
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 2148 additions and 715 deletions

View file

@ -47,17 +47,17 @@ Here are some references:
❌ Won't Merge
- A dedicated pr for translating existing languages (You can now translate on https://weblate.kuma.pet)
- Do not pass auto test
- Do not pass the auto test
- Any breaking changes
- Duplicated pull request
- Duplicated pull requests
- Buggy
- UI/UX is not close to Uptime Kuma
- Existing logic is completely modified or deleted for no reason
- A function that is completely out of scope
- Convert existing code into other programming languages
- Unnecessary large code changes (Hard to review, causes code conflicts to other pull requests)
- Modifications or deletions of existing logic without a valid reason.
- Adding functions that is completely out of scope
- Converting existing code into other programming languages
- Unnecessarily large code changes that are hard to review and cause conflicts with other PRs.
The above cases cannot cover all situations.
The above cases may not cover all possible situations.
I (@louislam) have the final say. If your pull request does not meet my expectations, I will reject it, no matter how much time you spend on it. Therefore, it is essential to have a discussion beforehand.

View file

@ -26,6 +26,8 @@ RUN chmod +x /app/extra/entrypoint.sh
FROM louislam/uptime-kuma:base-debian AS release
WORKDIR /app
ENV UPTIME_KUMA_IS_CONTAINER=1
# Copy app files from build layer
COPY --from=build /app /app

1858
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,13 @@
{
"name": "uptime-kuma",
"version": "1.22.0-beta.0",
"version": "1.22.0",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/louislam/uptime-kuma.git"
},
"engines": {
"node": "14.* || >=16.*"
"node": "14.* || 16.* || 18.*"
},
"scripts": {
"install-legacy": "npm install",
@ -39,7 +39,7 @@
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push",
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
"setup": "git checkout 1.21.3 && npm ci --production && npm run download-dist",
"setup": "git checkout 1.22.0 && npm ci --production && npm run download-dist",
"download-dist": "node extra/download-dist.js",
"mark-as-nightly": "node extra/mark-as-nightly.js",
"reset-password": "node extra/reset-password.js",
@ -84,7 +84,7 @@
"command-exists": "~1.2.9",
"compare-versions": "~3.6.0",
"compression": "~1.7.4",
"croner": "^6.0.3",
"croner": "~6.0.5",
"dayjs": "~1.11.5",
"dotenv": "~16.0.3",
"es6": "npm:@elastic/elasticsearch@^6.8.8",
@ -94,7 +94,7 @@
"express-basic-auth": "~1.2.1",
"express-static-gzip": "~2.1.7",
"form-data": "~4.0.0",
"gamedig": "^4.0.5",
"gamedig": "~4.0.5",
"http-graceful-shutdown": "~3.1.7",
"http-proxy-agent": "~5.0.0",
"https-proxy-agent": "~5.0.1",
@ -107,26 +107,29 @@
"mqtt": "~4.3.7",
"mssql": "~8.1.4",
"mysql2": "~2.3.3",
"nanoid": "^3.3.4",
"nanoid": "~3.3.4",
"node-cloudflared-tunnel": "~1.0.9",
"node-radius-client": "~1.0.0",
"nodemailer": "~6.6.5",
"nostr-tools": "^1.8.1",
"notp": "~2.0.3",
"password-hash": "~1.2.2",
"pg": "~8.8.0",
"pg-connection-string": "~2.5.0",
"playwright-core": "~1.35.1",
"prom-client": "~13.2.0",
"prometheus-api-metrics": "~3.2.1",
"protobufjs": "~7.1.1",
"qs": "~6.10.4",
"redbean-node": "~0.2.0",
"redbean-node": "~0.3.0",
"redis": "~4.5.1",
"socket.io": "~4.6.1",
"socket.io-client": "~4.6.1",
"socks-proxy-agent": "6.1.1",
"tar": "~6.1.11",
"tcp-ping": "~0.1.1",
"thirty-two": "~1.0.2"
"thirty-two": "~1.0.2",
"websocket-polyfill": "^0.0.3"
},
"devDependencies": {
"@actions/github": "~5.0.1",
@ -168,7 +171,7 @@
"qrcode": "~1.5.0",
"rollup-plugin-visualizer": "^5.6.0",
"sass": "~1.42.1",
"stylelint": "~14.7.1",
"stylelint": "~15.9.0",
"stylelint-config-standard": "~25.0.0",
"terser": "~5.15.0",
"timezones-list": "~3.0.1",

View file

@ -2,6 +2,7 @@ const basicAuth = require("express-basic-auth");
const passwordHash = require("./password-hash");
const { R } = require("redbean-node");
const { setting } = require("./util-server");
const { log } = require("../src/util");
const { loginRateLimiter, apiRateLimiter } = require("./rate-limiter");
const { Settings } = require("./settings");
const dayjs = require("dayjs");
@ -81,12 +82,16 @@ function apiAuthorizer(username, password, callback) {
apiRateLimiter.pass(null, 0).then((pass) => {
if (pass) {
verifyAPIKey(password).then((valid) => {
if (!valid) {
log.warn("api-auth", "Failed API auth attempt: invalid API Key");
}
callback(null, valid);
// Only allow a set number of api requests per minute
// (currently set to 60)
apiRateLimiter.removeTokens(1);
});
} else {
log.warn("api-auth", "Failed API auth attempt: rate limit exceeded");
callback(null, false);
}
});
@ -106,10 +111,12 @@ function userAuthorizer(username, password, callback) {
callback(null, user != null);
if (user == null) {
log.warn("basic-auth", "Failed basic auth attempt: invalid username/password");
loginRateLimiter.removeTokens(1);
}
});
} else {
log.warn("basic-auth", "Failed basic auth attempt: rate limit exceeded");
callback(null, false);
}
});

View file

@ -22,6 +22,8 @@ class Database {
*/
static uploadDir;
static screenshotDir;
static path;
/**
@ -105,6 +107,12 @@ class Database {
fs.mkdirSync(Database.uploadDir, { recursive: true });
}
// Create screenshot dir
Database.screenshotDir = Database.dataDir + "screenshots/";
if (! fs.existsSync(Database.screenshotDir)) {
fs.mkdirSync(Database.screenshotDir, { recursive: true });
}
log.info("db", `Data Dir: ${Database.dataDir}`);
}

View file

@ -20,6 +20,7 @@ const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
const { DockerHost } = require("../docker");
const { UptimeCacheList } = require("../uptime-cache-list");
const Gamedig = require("gamedig");
const jwt = require("jsonwebtoken");
/**
* status:
@ -70,6 +71,12 @@ class Monitor extends BeanModel {
const tags = await this.getTags();
let screenshot = null;
if (this.type === "real-browser") {
screenshot = "/screenshots/" + jwt.sign(this.id, UptimeKumaServer.getInstance().jwtSecret) + ".png";
}
let data = {
id: this.id,
name: this.name,
@ -117,7 +124,8 @@ class Monitor extends BeanModel {
radiusCalledStationId: this.radiusCalledStationId,
radiusCallingStationId: this.radiusCallingStationId,
game: this.game,
httpBodyEncoding: this.httpBodyEncoding
httpBodyEncoding: this.httpBodyEncoding,
screenshot,
};
if (includeSensitiveData) {
@ -740,7 +748,7 @@ class Monitor extends BeanModel {
} else if (this.type in UptimeKumaServer.monitorTypeList) {
let startTime = dayjs().valueOf();
const monitorType = UptimeKumaServer.monitorTypeList[this.type];
await monitorType.check(this, bean);
await monitorType.check(this, bean, UptimeKumaServer.getInstance());
if (!bean.ping) {
bean.ping = dayjs().valueOf() - startTime;
}
@ -1463,6 +1471,17 @@ class Monitor extends BeanModel {
return childrenIDs;
}
/**
* Unlinks all children of the the group monitor
* @param {number} groupID ID of group to remove children of
* @returns {Promise<void>}
*/
static async unlinkAllChildren(groupID) {
return await R.exec("UPDATE `monitor` SET parent = ? WHERE parent = ? ", [
null, groupID
]);
}
/**
* Checks recursive if parent (ancestors) are active
* @param {number} monitorID ID of the monitor to get

View file

@ -6,9 +6,10 @@ class MonitorType {
*
* @param {Monitor} monitor
* @param {Heartbeat} heartbeat
* @param {UptimeKumaServer} server
* @returns {Promise<void>}
*/
async check(monitor, heartbeat) {
async check(monitor, heartbeat, server) {
throw new Error("You need to override check()");
}

View file

@ -0,0 +1,164 @@
const { MonitorType } = require("./monitor-type");
const { chromium, Browser } = require("playwright-core");
const { UP, log } = require("../../src/util");
const { Settings } = require("../settings");
const commandExistsSync = require("command-exists").sync;
const childProcess = require("child_process");
const path = require("path");
const Database = require("../database");
const jwt = require("jsonwebtoken");
/**
*
* @type {Browser}
*/
let browser = null;
async function getBrowser() {
if (!browser) {
let executablePath = await Settings.get("chromeExecutable");
executablePath = await prepareChromeExecutable(executablePath);
browser = await chromium.launch({
//headless: false,
executablePath,
});
}
return browser;
}
async function prepareChromeExecutable(executablePath) {
// Special code for using the playwright_chromium
if (typeof executablePath === "string" && executablePath.toLocaleLowerCase() === "#playwright_chromium") {
executablePath = undefined;
} else if (!executablePath) {
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
executablePath = "/usr/bin/chromium";
// Install chromium in container via apt install
if ( !commandExistsSync(executablePath)) {
await new Promise((resolve, reject) => {
log.info("Chromium", "Installing Chromium...");
let child = childProcess.exec("apt update && apt --yes --no-install-recommends install chromium fonts-indic fonts-noto fonts-noto-cjk");
// On exit
child.on("exit", (code) => {
log.info("Chromium", "apt install chromium exited with code " + code);
if (code === 0) {
log.info("Chromium", "Installed Chromium");
let version = childProcess.execSync(executablePath + " --version").toString("utf8");
log.info("Chromium", "Chromium version: " + version);
resolve();
} else if (code === 100) {
reject(new Error("Installing Chromium, please wait..."));
} else {
reject(new Error("apt install chromium failed with code " + code));
}
});
});
}
} else if (process.platform === "win32") {
executablePath = findChrome([
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
"D:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
"D:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
"E:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
"E:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
]);
} else if (process.platform === "linux") {
executablePath = findChrome([
"chromium-browser",
"chromium",
"google-chrome",
]);
}
// TODO: Mac??
}
return executablePath;
}
function findChrome(executables) {
for (let executable of executables) {
if (commandExistsSync(executable)) {
return executable;
}
}
throw new Error("Chromium not found, please specify Chromium executable path in the settings page.");
}
async function resetChrome() {
if (browser) {
await browser.close();
browser = null;
}
}
/**
* Test if the chrome executable is valid and return the version
* @param executablePath
* @returns {Promise<string>}
*/
async function testChrome(executablePath) {
try {
executablePath = await prepareChromeExecutable(executablePath);
log.info("Chromium", "Testing Chromium executable: " + executablePath);
const browser = await chromium.launch({
executablePath,
});
const version = browser.version();
await browser.close();
return version;
} catch (e) {
throw new Error(e.message);
}
}
/**
* TODO: connect remote browser? https://playwright.dev/docs/api/class-browsertype#browser-type-connect
*
*/
class RealBrowserMonitorType extends MonitorType {
name = "real-browser";
async check(monitor, heartbeat, server) {
const browser = await getBrowser();
const context = await browser.newContext();
const page = await context.newPage();
const res = await page.goto(monitor.url, {
waitUntil: "networkidle",
timeout: monitor.interval * 1000 * 0.8,
});
let filename = jwt.sign(monitor.id, server.jwtSecret) + ".png";
await page.screenshot({
path: path.join(Database.screenshotDir, filename),
});
await context.close();
if (res.status() >= 200 && res.status() < 400) {
heartbeat.status = UP;
heartbeat.msg = res.status();
const timing = res.request().timing();
heartbeat.ping = timing.responseEnd;
} else {
throw new Error(res.status() + "");
}
}
}
module.exports = {
RealBrowserMonitorType,
testChrome,
resetChrome,
};

View file

@ -11,7 +11,7 @@ class HomeAssistant extends NotificationProvider {
try {
await axios.post(
`${notification.homeAssistantUrl}/api/services/notify/${notificationService}`,
`${notification.homeAssistantUrl.trim().replace(/\/*$/, "")}/api/services/notify/${notificationService}`,
{
title: "Uptime Kuma",
message,

View file

@ -0,0 +1,101 @@
const NotificationProvider = require("./notification-provider");
// polyfill for node <= 18
global.crypto = require("crypto");
const {
relayInit,
getPublicKey,
getEventHash,
signEvent,
nip04,
nip19
} = require("nostr-tools");
// polyfill for node <= 18
require("websocket-polyfill");
class Nostr extends NotificationProvider {
name = "nostr";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
// All DMs should have same timestamp
const createdAt = Math.floor(Date.now() / 1000);
const senderPrivateKey = await this.getPrivateKey(notification.sender);
const senderPublicKey = getPublicKey(senderPrivateKey);
const recipientsPublicKeys = await this.getPublicKeys(notification.recipients);
// Create NIP-04 encrypted direct message event for each recipient
const events = [];
for (const recipientPublicKey of recipientsPublicKeys) {
const ciphertext = await nip04.encrypt(senderPrivateKey, recipientPublicKey, msg);
let event = {
kind: 4,
pubkey: senderPublicKey,
created_at: createdAt,
tags: [[ "p", recipientPublicKey ]],
content: ciphertext,
};
event.id = getEventHash(event);
event.sig = signEvent(event, senderPrivateKey);
events.push(event);
}
// Publish events to each relay
const relays = notification.relays.split("\n");
let successfulRelays = 0;
// Connect to each relay
for (const relayUrl of relays) {
const relay = relayInit(relayUrl);
try {
await relay.connect();
successfulRelays++;
// Publish events
for (const event of events) {
relay.publish(event);
}
} catch (error) {
continue;
} finally {
relay.close();
}
}
// Report success or failure
if (successfulRelays === 0) {
throw Error("Failed to connect to any relays.");
}
return `${successfulRelays}/${relays.length} relays connected.`;
}
async getPrivateKey(sender) {
try {
const senderDecodeResult = await nip19.decode(sender);
const { data } = senderDecodeResult;
return data;
} catch (error) {
throw new Error(`Failed to get private key: ${error.message}`);
}
}
async getPublicKeys(recipients) {
const recipientsList = recipients.split("\n");
const publicKeys = [];
for (const recipient of recipientsList) {
try {
const recipientDecodeResult = await nip19.decode(recipient);
const { type, data } = recipientDecodeResult;
if (type === "npub") {
publicKeys.push(data);
} else {
throw new Error("not an npub");
}
} catch (error) {
throw new Error(`Error decoding recipient: ${error}`);
}
}
return publicKeys;
}
}
module.exports = Nostr;

View file

@ -20,6 +20,7 @@ const LineNotify = require("./notification-providers/linenotify");
const LunaSea = require("./notification-providers/lunasea");
const Matrix = require("./notification-providers/matrix");
const Mattermost = require("./notification-providers/mattermost");
const Nostr = require("./notification-providers/nostr");
const Ntfy = require("./notification-providers/ntfy");
const Octopush = require("./notification-providers/octopush");
const OneBot = require("./notification-providers/onebot");
@ -82,6 +83,7 @@ class Notification {
new LunaSea(),
new Matrix(),
new Mattermost(),
new Nostr(),
new Ntfy(),
new Octopush(),
new OneBot(),

View file

@ -149,6 +149,7 @@ const { Settings } = require("./settings");
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
const { pluginsHandler } = require("./socket-handlers/plugins-handler");
const apicache = require("./modules/apicache");
const { resetChrome } = require("./monitor-types/real-browser-monitor-type");
app.use(express.json());
@ -161,12 +162,6 @@ app.use(function (req, res, next) {
next();
});
/**
* Use for decode the auth object
* @type {null}
*/
let jwtSecret = null;
/**
* Show Setup Page
* @type {boolean}
@ -286,7 +281,7 @@ let needSetup = false;
log.info("auth", `Login by token. IP=${clientIP}`);
try {
let decoded = jwt.verify(token, jwtSecret);
let decoded = jwt.verify(token, server.jwtSecret);
log.info("auth", "Username from JWT: " + decoded.username);
@ -357,7 +352,7 @@ let needSetup = false;
ok: true,
token: jwt.sign({
username: data.username,
}, jwtSecret),
}, server.jwtSecret),
});
}
@ -387,7 +382,7 @@ let needSetup = false;
ok: true,
token: jwt.sign({
username: data.username,
}, jwtSecret),
}, server.jwtSecret),
});
} else {
@ -676,6 +671,7 @@ let needSetup = false;
// Edit a monitor
socket.on("editMonitor", async (monitor, callback) => {
try {
let removeGroupChildren = false;
checkLogin(socket);
let bean = await R.findOne("monitor", " id = ? ", [ monitor.id ]);
@ -684,7 +680,7 @@ let needSetup = false;
throw new Error("Permission denied.");
}
// Check if Parent is Decendant (would cause endless loop)
// Check if Parent is Descendant (would cause endless loop)
if (monitor.parent !== null) {
const childIDs = await Monitor.getAllChildrenIDs(monitor.id);
if (childIDs.includes(monitor.parent)) {
@ -692,6 +688,11 @@ let needSetup = false;
}
}
// Remove children if monitor type has changed (from group to non-group)
if (bean.type === "group" && monitor.type !== bean.type) {
removeGroupChildren = true;
}
bean.name = monitor.name;
bean.description = monitor.description;
bean.parent = monitor.parent;
@ -752,6 +753,10 @@ let needSetup = false;
await R.store(bean);
if (removeGroupChildren) {
await Monitor.unlinkAllChildren(monitor.id);
}
await updateMonitorNotification(bean.id, monitor.notificationIDList);
if (bean.isActive()) {
@ -1148,6 +1153,8 @@ let needSetup = false;
await doubleCheckPassword(socket, currentPassword);
}
const previousChromeExecutable = await Settings.get("chromeExecutable");
await setSettings("general", data);
server.entryPage = data.entryPage;
@ -1158,6 +1165,12 @@ let needSetup = false;
await server.setTimezone(data.serverTimezone);
}
// If Chrome Executable is changed, need to reset the browser
if (previousChromeExecutable !== data.chromeExecutable) {
log.info("settings", "Chrome executable is changed. Resetting Chrome...");
await resetChrome();
}
callback({
ok: true,
msg: "Saved"
@ -1697,7 +1710,7 @@ async function initDatabase(testMode = false) {
needSetup = true;
}
jwtSecret = jwtSecretBean.value;
server.jwtSecret = jwtSecretBean.value;
}
/**

View file

@ -3,6 +3,7 @@ const { Settings } = require("../settings");
const { sendInfo } = require("../client");
const { checkLogin } = require("../util-server");
const GameResolver = require("gamedig/lib/GameResolver");
const { testChrome } = require("../monitor-types/real-browser-monitor-type");
let gameResolver = new GameResolver();
let gameList = null;
@ -47,4 +48,18 @@ module.exports.generalSocketHandler = (socket, server) => {
});
});
socket.on("testChrome", (executable, callback) => {
// Just noticed that await call could block the whole socket.io server!!! Use pure promise instead.
testChrome(executable).then((version) => {
callback({
ok: true,
msg: "Found Chromium/Chrome. Version: " + version,
});
}).catch((e) => {
callback({
ok: false,
msg: e.message,
});
});
});
};

View file

@ -61,6 +61,12 @@ class UptimeKumaServer {
};
/**
* Use for decode the auth object
* @type {null}
*/
jwtSecret = null;
static getInstance(args) {
if (UptimeKumaServer.instance == null) {
UptimeKumaServer.instance = new UptimeKumaServer(args);
@ -98,11 +104,17 @@ class UptimeKumaServer {
}
}
// Set Monitor Types
UptimeKumaServer.monitorTypeList["real-browser"] = new RealBrowserMonitorType();
this.io = new Server(this.httpServer);
}
/** Initialise app after the database has been set up */
async initAfterDatabaseReady() {
// Static
this.app.use("/screenshots", express.static(Database.screenshotDir));
await CacheableDnsHttpAgent.update();
process.env.TZ = await this.getTimezone();
@ -337,3 +349,4 @@ module.exports = {
// Must be at the end
const { MonitorType } = require("./monitor-types/monitor-type");
const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor-type");

View file

@ -448,12 +448,18 @@ exports.radius = function (
exports.redisPingAsync = function (dsn) {
return new Promise((resolve, reject) => {
const client = redis.createClient({
url: dsn,
url: dsn
});
client.on("error", (err) => {
if (client.isOpen) {
client.disconnect();
}
reject(err);
});
client.connect().then(() => {
if (!client.isOpen) {
client.emit("error", new Error("connection isn't open"));
}
client.ping().then((res, err) => {
if (client.isOpen) {
client.disconnect();
@ -463,7 +469,7 @@ exports.redisPingAsync = function (dsn) {
} else {
resolve(res);
}
});
}).catch(error => reject(error));
});
});
};

View file

@ -126,6 +126,7 @@ export default {
"lunasea": "LunaSea",
"matrix": "Matrix",
"mattermost": "Mattermost",
"nostr": "Nostr",
"ntfy": "Ntfy",
"octopush": "Octopush",
"OneBot": "OneBot",

View file

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

View file

@ -18,6 +18,7 @@ import LineNotify from "./LineNotify.vue";
import LunaSea from "./LunaSea.vue";
import Matrix from "./Matrix.vue";
import Mattermost from "./Mattermost.vue";
import Nostr from "./Nostr.vue";
import Ntfy from "./Ntfy.vue";
import Octopush from "./Octopush.vue";
import OneBot from "./OneBot.vue";
@ -75,6 +76,7 @@ const NotificationFormList = {
"lunasea": LunaSea,
"matrix": Matrix,
"mattermost": Mattermost,
"nostr": Nostr,
"ntfy": Ntfy,
"octopush": Octopush,
"OneBot": OneBot,

View file

@ -190,6 +190,30 @@
</div>
</div>
<!-- Chrome Executable -->
<div class="mb-4">
<label class="form-label" for="primaryBaseURL">
{{ $t("chromeExecutable") }}
</label>
<div class="input-group mb-3">
<input
id="primaryBaseURL"
v-model="settings.chromeExecutable"
class="form-control"
name="primaryBaseURL"
:placeholder="$t('chromeExecutableAutoDetect')"
/>
<button class="btn btn-outline-primary" type="button" @click="testChrome">
{{ $t("Test") }}
</button>
</div>
<div class="form-text">
{{ $t("chromeExecutableDescription") }}
</div>
</div>
<!-- Save Button -->
<div>
<button class="btn btn-primary" type="submit">
@ -241,6 +265,12 @@ export default {
autoGetPrimaryBaseURL() {
this.settings.primaryBaseURL = location.protocol + "//" + location.host;
},
testChrome() {
this.$root.getSocket().emit("testChrome", this.settings.chromeExecutable, (res) => {
this.$root.toastRes(res);
});
},
},
};
</script>

View file

@ -776,5 +776,13 @@
"Badge Suffix": "Суфикс на баджа",
"Badge Label Prefix": "Префикс на етикета на значката",
"Badge Pending Color": "Цвят на баджа за изчакващ",
"Badge Down Days": "Колко дни баджът да не се показва"
"Badge Down Days": "Колко дни баджът да не се показва",
"Group": "Група",
"Monitor Group": "Монитор група",
"Cannot connect to the socket server": "Не може да се свърже със сокет сървъра",
"Reconnecting...": "Повторно свързване...",
"Edit Maintenance": "Редактиране на поддръжка",
"Home": "Главна страница",
"noGroupMonitorMsg": "Не е налично. Първо създайте групов монитор.",
"Close": "Затвори"
}

28
src/lang/ca.json Normal file
View file

@ -0,0 +1,28 @@
{
"Settings": "Paràmetres",
"Dashboard": "Tauler",
"Help": "Ajuda",
"New Update": "Nova actualització",
"Language": "Idioma",
"Appearance": "Aparença",
"Theme": "Tema",
"General": "General",
"Game": "Joc",
"Version": "Versió",
"Check Update On GitHub": "Comprovar actualitzacions a GitHub",
"List": "Llista",
"Home": "Inici",
"Add": "Afegir",
"Add New Monitor": "Afegir nou monitor",
"Quick Stats": "Estadístiques ràpides",
"Up": "Funcional",
"Down": "Caigut",
"Pending": "Pendent",
"Maintenance": "Manteniment",
"Unknown": "Desconegut",
"Cannot connect to the socket server": "No es pot connectar al servidor socket",
"Reconnecting...": "S'està tornant a connectar...",
"languageName": "Català",
"Primary Base URL": "URL Base Primària",
"statusMaintenance": "Manteniment"
}

View file

@ -1 +1,46 @@
{}
{
"languageName": "کوردی",
"Settings": "ڕێکخستنەکان",
"Help": "یارمەتی",
"New Update": "وەشانی نوێ",
"Language": "زمان",
"Appearance": "ڕووکار",
"Theme": "شێوەی ڕووکار",
"General": "گشتی",
"Game": "یاری",
"Version": "وەشان",
"Check Update On GitHub": "سەیری وەشانی نوێ بکە لە Github",
"List": "لیست",
"Add": "زیادکردن",
"Quick Stats": "ئاماری خێرا",
"Up": "سەروو",
"Down": "خواروو",
"Pending": "هەڵپەسێردراو",
"statusMaintenance": "چاکردنەوە",
"Maintenance": "چاکردنەوە",
"Unknown": "نەزانراو",
"Passive Monitor Type": "جۆری مۆنیتەری پاسیڤ",
"Specific Monitor Type": "جۆری مۆنیتەری تایبەت",
"markdownSupported": "ڕستەسازی مارکداون پشتگیری دەکرێت",
"pauseDashboardHome": "وچان",
"Pause": "وچان",
"Name": "ناو",
"Status": "دۆخ",
"Message": "پەیام",
"No important events": "هیچ ڕووداوێکی گرنگ نییە",
"Resume": "‬دەستپێکردنەوە",
"Edit": "بژارکردن",
"Delete": "سڕینەوە",
"Uptime": "کاتی کارکردن",
"Cert Exp.": "بەسەرچوونی بڕوانامەی SSL.",
"day": "ڕۆژ | ڕۆژەکان",
"-day": "-ڕۆژ",
"hour": "کاتژمێر",
"Dashboard": "داشبۆرد",
"Primary Base URL": "بەستەری بنچینەیی سەرەکی",
"Add New Monitor": "مۆنیتەرێکی نوێ زیاد بکە",
"General Monitor Type": "جۆری مۆنیتەری گشتی",
"DateTime": "رێکەوت",
"Current": "هەنووکە",
"Monitor": "مۆنیتەر | مۆنیتەرەکان"
}

View file

@ -757,11 +757,11 @@
"Show Clickable Link Description": "Pokud je zaškrtnuto, všichni, kdo mají přístup k této stavové stránce, mají přístup k adrese URL monitoru.",
"Open Badge Generator": "Otevřít generátor odznaků",
"Badge Type": "Typ odznaku",
"Badge Duration": "Délka platnosti odznaku",
"Badge Duration": "Platnost odznaku",
"Badge Label": "Štítek odznaku",
"Badge Prefix": "Prefix odznaku",
"Monitor Setting": "{0}'s Nastavení dohledu",
"Badge Generator": "{0}'s Generátor odznaků",
"Badge Generator": "Generátor odznaků pro {0}",
"Badge Label Color": "Barva štítku odznaku",
"Badge Color": "Barva odznaku",
"Badge Style": "Styl odznaku",
@ -769,9 +769,20 @@
"Badge URL": "URL odznaku",
"Badge Suffix": "Přípona odznaku",
"Badge Label Prefix": "Prefix štítku odznaku",
"Badge Up Color": "Barva odzanaku při Běží",
"Badge Up Color": "Barva odznaku při Běží",
"Badge Down Color": "Barva odznaku při Nedostupné",
"Badge Pending Color": "Barva odznaku při Pauze",
"Badge Maintenance Color": "Barva odznaku při Údržbě",
"Badge Warn Color": "Barva odznaku při Upozornění"
"Badge Warn Color": "Barva odznaku při Upozornění",
"Reconnecting...": "Obnovení spojení...",
"Cannot connect to the socket server": "Nelze se připojit k soketovému serveru",
"Edit Maintenance": "Upravit Údržbu",
"Home": "Hlavní stránka",
"Badge Down Days": "Odznak nedostupných dní",
"Group": "Skupina",
"Monitor Group": "Sledovaná skupina",
"noGroupMonitorMsg": "Není k dispozici. Nejprve vytvořte skupin dohledů.",
"Close": "Zavřít",
"Badge value (For Testing only.)": "Hodnota odznaku (pouze pro testování)",
"Badge Warn Days": "Odznak dní s upozorněním"
}

View file

@ -776,5 +776,10 @@
"Badge Label Suffix": "Badge Label Suffix",
"Badge value (For Testing only.)": "Badge Wert (nur für Tests)",
"Show Clickable Link Description": "Wenn diese Option aktiviert ist, kann jeder, der Zugriff auf diese Statusseite hat, auf die Monitor URL zugreifen.",
"Badge Down Color": "Badge Down Farbe"
"Badge Down Color": "Badge Down Farbe",
"Edit Maintenance": "Wartung bearbeiten",
"Group": "Gruppe",
"Monitor Group": "Monitor Gruppe",
"noGroupMonitorMsg": "Nicht verfügbar. Erstelle zunächst einen Gruppenmonitor.",
"Close": "Schliessen"
}

View file

@ -782,5 +782,7 @@
"Badge Suffix": "Badge Suffix",
"Badge Warn Days": "Badge Warnung Tage",
"Group": "Gruppe",
"Monitor Group": "Monitor Gruppe"
"Monitor Group": "Monitor Gruppe",
"noGroupMonitorMsg": "Nicht verfügbar. Erstelle zunächst einen Gruppenmonitor.",
"Close": "Schließen"
}

View file

@ -435,6 +435,9 @@
"Enable DNS Cache": "Enable DNS Cache",
"Enable": "Enable",
"Disable": "Disable",
"chromeExecutable": "Chrome/Chromium Executable",
"chromeExecutableAutoDetect": "Auto Detect",
"chromeExecutableDescription": "For Docker users, if Chromium is not yet installed, it may take a few minutes to install and display the test result. It takes 1GB of disk space.",
"dnsCacheDescription": "It may be not working in some IPv6 environments, disable it if you encounter any issues.",
"Single Maintenance Window": "Single Maintenance Window",
"Maintenance Time Window of a Day": "Maintenance Time Window of a Day",
@ -751,5 +754,11 @@
"Badge URL": "Badge URL",
"Group": "Group",
"Monitor Group": "Monitor Group",
"noGroupMonitorMsg": "Not Available. Create a Group Monitor First."
"noGroupMonitorMsg": "Not Available. Create a Group Monitor First.",
"Close": "Close",
"nostrRelays": "Nostr relays",
"nostrRelaysHelp": "One relay URL per line",
"nostrSender": "Sender Private Key (nsec)",
"nostrRecipients": "Recipients Public Keys (npub)",
"nostrRecipientsHelp": "npub format, one per line"
}

View file

@ -751,5 +751,7 @@
"statusPageRefreshIn": "Reinicio en: {0}",
"twilioAuthToken": "Token de Autentificación",
"ntfyUsernameAndPassword": "Nombre de Usuario y Contraseña",
"ntfyAuthenticationMethod": "Método de Autentificación"
"ntfyAuthenticationMethod": "Método de Autentificación",
"Cannot connect to the socket server": "No se puede conectar al servidor socket",
"Reconnecting...": "Reconectando..."
}

View file

@ -745,5 +745,13 @@
"Show Clickable Link Description": "اگر انتخاب شود، همه کسانی که به این صفحه وضعیت دسترسی دارند میتوانند به صفحه مانیتور نیز دسترسی داشته باشند.",
"Badge Up Color": "رنگ نشان زمانی که مانیتور بدون مشکل و بالا است",
"Badge Pending Color": "رنگ نشان زمانی که مانیتور در حال انتظار است",
"Badge Warn Days": "روزهایی که مانیتور در حالت هشدار است"
"Badge Warn Days": "روزهایی که مانیتور در حالت هشدار است",
"noGroupMonitorMsg": "موجود نیست. ابتدا یک گروه مانیتور جدید ایجاد کنید.",
"Home": "خانه",
"Edit Maintenance": "ویرایش تعمیر و نگهداری",
"Cannot connect to the socket server": "عدم امکان ارتباط با سوکت سرور",
"Reconnecting...": "ارتباط مجدد...",
"Monitor Group": "گروه مانیتور",
"Group": "گروه",
"Close": "بستن"
}

View file

@ -59,7 +59,7 @@
"Add New Monitor": "Ajouter une nouvelle sonde",
"Quick Stats": "Résumé",
"Up": "En ligne",
"Down": "Bas",
"Down": "Hors ligne",
"Pending": "En attente",
"Unknown": "Inconnu",
"Pause": "En pause",
@ -88,8 +88,8 @@
"Port": "Port",
"Heartbeat Interval": "Intervalle de vérification",
"Retries": "Essais",
"Heartbeat Retry Interval": "Réessayer l'intervalle de vérification",
"Resend Notification if Down X times consecutively": "Renvoyer la notification si en panne X fois consécutivement",
"Heartbeat Retry Interval": "Intervalle de ré-essaie",
"Resend Notification if Down X times consecutively": "Renvoyer la notification si hors ligne X fois consécutivement",
"Advanced": "Avancé",
"Upside Down Mode": "Mode inversé",
"Max. Redirects": "Nombre maximum de redirections",
@ -775,5 +775,14 @@
"Monitor Setting": "Réglage de la sonde {0}",
"Badge Generator": "Générateur de badges {0}",
"Badge Label": "Étiquette de badge",
"Badge URL": "URL du badge"
"Badge URL": "URL du badge",
"Cannot connect to the socket server": "Impossible de se connecter au serveur de socket",
"Reconnecting...": "Reconnexion...",
"Edit Maintenance": "Modifier la maintenance",
"Monitor Group": "Groupe de sonde | Groupe de sondes",
"Badge Down Days": "Badge hors ligne",
"Group": "Groupe",
"Home": "Accueil",
"noGroupMonitorMsg": "Pas disponible. Créez d'abord une sonde de groupe.",
"Close": "Fermer"
}

23
src/lang/gl.json Normal file
View file

@ -0,0 +1,23 @@
{
"Settings": "Axustes",
"Dashboard": "Panel",
"Help": "Axuda",
"General": "Xeral",
"List": "Lista",
"Home": "Casa",
"Add": "Engadir",
"Up": "Arriba",
"Pending": "Pendente",
"statusMaintenance": "Mantemento",
"Maintenance": "Mantemento",
"Unknown": "Descoñecido",
"Reconnecting...": "Reconectando...",
"pauseDashboardHome": "Pausa",
"Pause": "Pausa",
"Name": "Nome",
"Status": "Estado",
"DateTime": "DataHora",
"Message": "Mensaxe",
"languageName": "Galego",
"Down": "Abaixo"
}

View file

@ -724,5 +724,22 @@
"Edit Tag": "עריכת תגית",
"Learn More": "לקריאה נוספת",
"telegramSendSilently": "שליחה שקטה",
"telegramSendSilentlyDescription": "שליחת הודעות שקטה. משתמשים יקבלו ההתראה ללא צליל."
"telegramSendSilentlyDescription": "שליחת הודעות שקטה. משתמשים יקבלו ההתראה ללא צליל.",
"Add New Tag": "הוסף תג חדש",
"Home": "ראשי",
"sameAsServerTimezone": "אותו איזור זמן כמו השרת",
"cronSchedule": "לו\"ז: ",
"twilioToNumber": "למספר",
"startDateTime": "תאריך\\זמן התחלה",
"pagertreeSilent": "שקט",
"Reconnecting...": "מתחבר מחדש...",
"statusPageRefreshIn": "רענון תוך: {0}",
"Edit Maintenance": "ערוך תחזוקה",
"pagertreeUrgency": "דחיפות",
"pagertreeLow": "נמוכה",
"pagertreeMedium": "בינונית",
"pagertreeHigh": "גבוהה",
"pagertreeCritical": "קריטי",
"pagertreeResolve": "הגדרה אוטומטית",
"ntfyUsernameAndPassword": "שם משתמש וסיסמא"
}

43
src/lang/hi.json Normal file
View file

@ -0,0 +1,43 @@
{
"Dashboard": "डैशबोर्ड",
"Help": "मदद",
"New Update": "नया अपडेट",
"Language": "भाषा",
"Appearance": "अपीयरेंस",
"Theme": "थीम",
"Game": "गेम",
"languageName": "हिंदी",
"Settings": "सेटिंग्स",
"General": "जनरल",
"List": "सूची",
"Add": "जोड़ें",
"Add New Monitor": "नया मॉनिटर जोड़ें",
"Pending": "लंबित",
"statusMaintenance": "रखरखाव",
"Maintenance": "रखरखाव",
"Unknown": "अज्ञात",
"Cannot connect to the socket server": "सॉकेट सर्वर से कनेक्ट नहीं हो सकता",
"pauseDashboardHome": "विराम",
"Resume": "फिर से शुरू करें",
"Delete": "हटाएं",
"Current": "मौजूदा",
"Up": "चालू",
"General Monitor Type": "सामान्य मॉनिटर प्रकार",
"Specific Monitor Type": "विशिष्ट मॉनिटर प्रकार",
"Pause": "विराम",
"Name": "नाम",
"Message": "संदेश",
"No important events": "कोई महत्वपूर्ण घटनाक्रम नहीं",
"Edit": "परिवर्तन",
"Ping": "पिंग",
"Monitor Type": "मॉनिटर प्रकार",
"Keyword": "कीवर्ड",
"Friendly Name": "दोस्ताना नाम",
"Version": "संस्करण",
"Home": "घर",
"Quick Stats": "शीघ्र आँकड़े",
"Reconnecting...": "पुनः कनेक्ट किया जा रहा है...",
"Down": "बंद",
"Passive Monitor Type": "निष्क्रिय मॉनिटर प्रकार",
"Status": "स्थिति"
}

View file

@ -751,5 +751,13 @@
"endDateTime": "Data/godzina zakończenia",
"cronExpression": "Wyrażenie Cron",
"ntfyAuthenticationMethod": "Metoda Uwierzytelnienia",
"ntfyUsernameAndPassword": "Nazwa użytkownika i hasło"
"ntfyUsernameAndPassword": "Nazwa użytkownika i hasło",
"noGroupMonitorMsg": "Niedostępna. Stwórz najpierw grupę monitorów.",
"Close": "Zamknij",
"pushoverMessageTtl": "TTL wiadomości (sekundy)",
"Home": "Strona główna",
"Group": "Grupa",
"Monitor Group": "Grupa monitora",
"Reconnecting...": "Ponowne łączenie...",
"Cannot connect to the socket server": "Nie można połączyć się z serwerem gniazda"
}

View file

@ -6,7 +6,7 @@
"upsideDownModeDescription": "Реверс статуса сервиса. Если сервис доступен, то он помечается как НЕДОСТУПНЫЙ.",
"maxRedirectDescription": "Максимальное количество перенаправлений. Поставьте 0, чтобы отключить перенаправления.",
"acceptedStatusCodesDescription": "Выберите коды статусов для определения доступности сервиса.",
"passwordNotMatchMsg": "Повтор пароля не совпадает.",
"passwordNotMatchMsg": "Введёные пароли не совпадают",
"notificationDescription": "Привяжите уведомления к мониторам.",
"keywordDescription": "Поиск слова в чистом HTML или в JSON-ответе (чувствительно к регистру).",
"pauseDashboardHome": "Пауза",
@ -43,7 +43,7 @@
"Delete": "Удалить",
"Current": "Текущий",
"Uptime": "Аптайм",
"Cert Exp.": "Сертификат истекает.",
"Cert Exp.": "Сертификат истекает",
"day": "день | дней",
"-day": "-дней",
"hour": "час",
@ -69,7 +69,7 @@
"Light": "Светлая",
"Dark": "Тёмная",
"Auto": "Авто",
"Theme - Heartbeat Bar": "Тема - Полоса частоты опроса",
"Theme - Heartbeat Bar": "Полоса частоты опроса",
"Normal": "Обычный",
"Bottom": "Снизу",
"None": "Отсутствует",
@ -160,7 +160,7 @@
"Tag with this name already exist.": "Такой тег уже существует.",
"Tag with this value already exist.": "Тег с таким значением уже существует.",
"color": "цвет",
"value (optional)": "значение (опционально)",
"value (optional)": "значение (необязательно)",
"Gray": "Серый",
"Red": "Красный",
"Orange": "Оранжевый",
@ -175,9 +175,9 @@
"Entry Page": "Главная страница",
"statusPageNothing": "Здесь пусто. Добавьте группу или монитор.",
"No Services": "Нет сервисов",
"All Systems Operational": "Все системы работают в штатном режиме",
"Partially Degraded Service": "Сервисы работают частично",
"Degraded Service": "Все сервисы не работают",
"All Systems Operational": "Все системы работают",
"Partially Degraded Service": "Частичная работа сервисов",
"Degraded Service": "Отказ всех сервисов",
"Add Group": "Добавить группу",
"Add a monitor": "Добавить монитор",
"Edit Status Page": "Редактировать",
@ -212,7 +212,7 @@
"pushOptionalParams": "Опциональные параметры: {0}",
"defaultNotificationName": "Моё уведомление {notification} ({number})",
"here": "здесь",
"Required": "Требуется",
"Required": "Обязательно",
"Bot Token": "Токен бота",
"wayToGetTelegramToken": "Вы можете взять токен здесь - {0}.",
"Chat ID": "ID чата",
@ -296,7 +296,7 @@
"promosmsPhoneNumber": "Номер телефона (для получателей из Польши можно пропустить код региона)",
"promosmsSMSSender": "Имя отправителя SMS: Зарегистрированное или одно из имён по умолчанию: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
"Feishu WebHookUrl": "Feishu WebHookURL",
"matrixHomeserverURL": "URL сервера (вместе с http(s):// и опционально порт)",
"matrixHomeserverURL": "URL сервера (вместе с http(s):// и по желанию порт)",
"Internal Room Id": "Внутренний ID комнаты",
"matrixDesc1": "Внутренний ID комнаты можно найти в Подробностях в параметрах канала вашего Matrix клиента. Он должен выглядеть примерно как !QMdRCpUIfLwsfjxye6:home.server.",
"matrixDesc2": "Рекомендуется создать нового пользователя и не использовать токен доступа личного пользователя Matrix, т.к. это влечёт за собой полный доступ к аккаунту и к комнатам, в которых вы состоите. Вместо этого создайте нового пользователя и пригласите его только в ту комнату, в которой вы хотите получать уведомления. Токен доступа можно получить, выполнив команду {0}",
@ -335,9 +335,9 @@
"Current User": "Текущий пользователь",
"About": "О программе",
"Description": "Описание",
"Powered by": "Работает на основе скрипта от",
"Powered by": "Работает на",
"shrinkDatabaseDescription": "Включает VACUUM для базы данных SQLite. Если ваша база данных была создана на версии 1.10.0 и более, AUTO_VACUUM уже включен и это действие не требуется.",
"deleteStatusPageMsg": "Вы действительно хотите удалить эту страницу статуса сервисов?",
"deleteStatusPageMsg": "Вы действительно хотите удалить эту страницу статуса?",
"Style": "Стиль",
"info": "ИНФО",
"warning": "ВНИМАНИЕ",
@ -367,7 +367,7 @@
"Pick Accepted Status Codes...": "Выберите принятые коды состояния…",
"Default": "По умолчанию",
"Please input title and content": "Пожалуйста, введите название и содержание",
"Last Updated": "Последнее Обновление",
"Last Updated": "Последнее обновление",
"Untitled Group": "Группа без названия",
"Services": "Сервисы",
"serwersms": "SerwerSMS.pl",
@ -379,11 +379,11 @@
"smtpDkimSettings": "DKIM Настройки",
"smtpDkimDesc": "Пожалуйста ознакомьтесь с {0} Nodemailer DKIM для использования.",
"documentation": "документацией",
"smtpDkimDomain": "Имя Домена",
"smtpDkimDomain": "Имя домена",
"smtpDkimKeySelector": "Ключ",
"smtpDkimPrivateKey": "Приватный ключ",
"smtpDkimHashAlgo": "Алгоритм хэша (опционально)",
"smtpDkimheaderFieldNames": "Заголовок ключей для подписи (опционально)",
"smtpDkimHashAlgo": "Алгоритм хэша (необязательно)",
"smtpDkimheaderFieldNames": "Заголовок ключей для подписи (необязательно)",
"smtpDkimskipFields": "Заголовок ключей не для подписи (опционально)",
"gorush": "Gorush",
"alerta": "Alerta",
@ -439,9 +439,9 @@
"Uptime Kuma": "Uptime Kuma",
"Slug": "Slug",
"Accept characters:": "Принимаемые символы:",
"startOrEndWithOnly": "Начинается или кончается только {0}",
"startOrEndWithOnly": "Начинается или заканчивается только на {0}",
"No consecutive dashes": "Без последовательных тире",
"The slug is already taken. Please choose another slug.": "Слово уже занято. Пожалуйста, выберите другой вариант.",
"The slug is already taken. Please choose another slug.": "Этот slug уже занят. Пожалуйста, выберите другой.",
"Page Not Found": "Страница не найдена",
"wayToGetCloudflaredURL": "(Скачать cloudflared с {0})",
"cloudflareWebsite": "Веб-сайт Cloudflare",
@ -467,7 +467,7 @@
"onebotMessageType": "Тип сообщения OneBot",
"onebotGroupMessage": "Группа",
"onebotPrivateMessage": "Private",
"onebotUserOrGroupId": "ID группы или пользователя",
"onebotUserOrGroupId": "ID группы/пользователя",
"onebotSafetyTips": "В целях безопасности необходимо установить токен доступа",
"PushDeer Key": "ключ PushDeer",
"Footer Text": "Текст нижнего колонтитула",
@ -568,7 +568,7 @@
"goAlertInfo": "GoAlert — это приложение с открытым исходным кодом для составления расписания вызовов, автоматической эскалации и уведомлений (например, SMS или голосовых звонков). Автоматически привлекайте нужного человека, нужным способом и в нужное время! {0}",
"goAlertIntegrationKeyInfo": "Получить общий ключ интеграции API для сервиса в этом формате \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" обычно значение параметра токена скопированного URL.",
"goAlert": "GoAlert",
"backupOutdatedWarning": "Устарело: поскольку добавлено множество функций, а эта функция резервного копирования немного не поддерживается, она не может создать или восстановить полную резервную копию.",
"backupOutdatedWarning": "Устарело: эта функция резервного копирования более не поддерживается. Поскольку добавлено множество функций, она не может создать или восстановить полную резервную копию.",
"backupRecommend": "Сделайте резервную копию тома или папки с данными (./data/) напрямую.",
"Optional": "Необязательно",
"squadcast": "Squadcast",
@ -578,24 +578,24 @@
"SMSManager": "SMSManager",
"You can divide numbers with": "Вы можете делить числа с",
"or": "или",
"Maintenance": "Обслуживание",
"Schedule maintenance": "Запланировать обслуживание",
"affectedMonitorsDescription": "Выберите мониторы, которые будут затронуты во время обслуживания",
"affectedStatusPages": "Показывать уведомление об обслуживании на выбранных страницах статуса",
"Maintenance": "Техобслуживание",
"Schedule maintenance": "Запланировать техобслуживание",
"affectedMonitorsDescription": "Выберите мониторы, которые будут затронуты во время техбслуживания",
"affectedStatusPages": "Показывать уведомление о техбслуживании на выбранных страницах статуса",
"atLeastOneMonitor": "Выберите больше одного затрагиваемого монитора",
"dnsPortDescription": "По умолчанию порт DNS сервера - 53. Мы можете изменить его в любое время.",
"Monitor": "Монитор | Мониторы",
"webhookAdditionalHeadersTitle": "Дополнительные Заголовки",
"recurringIntervalMessage": "Запускать 1 раз каждый день | Запускать 1 раз каждые {0} дней",
"error": "ошибка",
"statusMaintenance": "Обслуживание",
"statusMaintenance": "Техобслуживание",
"Affected Monitors": "Затронутые мониторы",
"Start of maintenance": "Начало обслуживания",
"Start of maintenance": "Начало техобслуживания",
"All Status Pages": "Все страницы статусов",
"Select status pages...": "Выберите страницу статуса…",
"resendEveryXTimes": "Повтор каждые {0} раз",
"resendDisabled": "Повторная отправка отключена",
"deleteMaintenanceMsg": "Вы действительно хотите удалить это обслуживание?",
"deleteMaintenanceMsg": "Вы действительно хотите удалить это техбслуживание?",
"critical": "критично",
"Custom Monitor Type": "Собственный тип монитора",
"markdownSupported": "Поддерживает синтаксис Markdown",
@ -630,7 +630,7 @@
"lastDay2": "Второй последний день месяца",
"lastDay3": "Третий последний день месяца",
"lastDay4": "Четвертый последний день месяца",
"No Maintenance": "Без обслуживания",
"No Maintenance": "Нет техбслуживаний",
"pauseMaintenanceMsg": "Вы уверены что хотите поставить на паузу?",
"maintenanceStatus-under-maintenance": "На техобслуживании",
"maintenanceStatus-inactive": "Неактивен",
@ -640,13 +640,13 @@
"Display Timezone": "Показать часовой пояс",
"Server Timezone": "Часовой пояс сервера",
"statusPageMaintenanceEndDate": "Конец",
"IconUrl": "URL Иконки",
"IconUrl": "URL иконки",
"Enable DNS Cache": "Включить DNS кэш",
"Enable": "Включить",
"Disable": "Отключить",
"Single Maintenance Window": "Единое Окно Обслуживания",
"Schedule Maintenance": "Запланировать обслуживание",
"Date and Time": "Дата и Время",
"Single Maintenance Window": "Единое окно техбслуживания",
"Schedule Maintenance": "Запланировать техбслуживание",
"Date and Time": "Дата и время",
"DateTime Range": "Промежуток даты и времени",
"uninstalling": "Удаляется",
"dataRetentionTimeError": "Период хранения должен быть равен 0 или больше",
@ -676,10 +676,10 @@
"Integration URL": "URL интеграции",
"do nothing": "ничего не делать",
"smseagleTo": "Номер(а) телефона",
"smseagleGroup": "Название(я) групп телефонной книги",
"smseagleContact": "Имена контактов из телефонной книжки",
"smseagleGroup": "Название(я) группы телефонной книги",
"smseagleContact": "Имена контактов телефонной книги",
"smseagleRecipientType": "Тип получателя",
"smseagleRecipient": "Получатель(я) (через запятую, если необходимо указать несколько)",
"smseagleRecipient": "Получатель(и) (если множество, должны быть разделены запятой)",
"smseagleToken": "Токен доступа API",
"smseagleUrl": "URL вашего SMSEagle устройства",
"smseagleEncoding": "Отправить в юникоде",
@ -695,7 +695,7 @@
"telegramProtectContentDescription": "Если включено, сообщения бота в Telegram будут запрещены для пересылки и сохранения.",
"telegramSendSilently": "Отправить без звука",
"telegramSendSilentlyDescription": "Пользователи получат уведомление без звука.",
"Maintenance Time Window of a Day": "Суточный интервал для обслуживания",
"Maintenance Time Window of a Day": "Суточный интервал для техбслуживания",
"Clone Monitor": "Копия",
"Clone": "Копия",
"cloneOf": "Копия {0}",
@ -703,31 +703,31 @@
"Add New Tag": "Добавить тег",
"Body Encoding": "Тип содержимого запроса.(JSON or XML)",
"Strategy": "Стратегия",
"Free Mobile User Identifier": "Бесплатный идентификатор мобильного пользователя",
"Free Mobile User Identifier": "Бесплатный мобильный идентификатор пользователя",
"Auto resolve or acknowledged": "Автоматическое разрешение или подтверждение",
"auto acknowledged": "автоматическое подтверждение",
"auto resolve": "автоматическое разрешение",
"API Keys": "Ключи API",
"Expiry": "Истекает",
"Expiry date": "Дата окончания действия",
"Expiry": "Срок действия",
"Expiry date": "Дата истечения срока действия",
"Don't expire": "Не истекает",
"Continue": "Продолжать",
"Add Another": "Добавьте еще один",
"Continue": "Продолжить",
"Add Another": "Добавить еще",
"Key Added": "Ключ добавлен",
"Add API Key": "Добавить ключ API",
"No API Keys": "Нет API ключей",
"Add API Key": "Добавить API ключ",
"No API Keys": "Нет ключей API",
"apiKey-active": "Активный",
"apiKey-expired": "Истёк",
"apiKey-inactive": "Неактивный",
"Expires": "Истекает",
"disableAPIKeyMsg": "Вы уверены, что хотите отключить этот ключ?",
"disableAPIKeyMsg": "Вы уверены, что хотите отключить этот API ключ?",
"Generate": "Сгенерировать",
"pagertreeResolve": "Автоматическое разрешение",
"pagertreeDoNothing": "ничего не делать",
"pagertreeDoNothing": "Ничего не делать",
"lunaseaTarget": "Цель",
"lunaseaDeviceID": "Идентификатор устройства",
"lunaseaUserID": "Идентификатор пользователя",
"Lowcost": "Низкая стоимость",
"Lowcost": "Бюджетный",
"pagertreeIntegrationUrl": "URL-адрес интеграции",
"pagertreeUrgency": "Срочность",
"pagertreeSilent": "Тихий",
@ -736,15 +736,15 @@
"pagertreeHigh": "Высокий",
"pagertreeCritical": "Критический",
"high": "высокий",
"promosmsAllowLongSMS": "Разрешить длинные SMS-сообщения",
"promosmsAllowLongSMS": "Разрешить длинные СМС",
"Economy": "Экономия",
"wayToGetPagerDutyKey": "Вы можете получить это, перейдя в службу -> Каталог служб -> (Выберите службу) -> Интеграции -> Добавить интеграцию. Здесь вы можете выполнить поиск по \"Events API V2\". Дополнительная информация {0}",
"apiKeyAddedMsg": "Ваш API ключ был добавлен. Пожалуйста, запишите это, так как оно больше не будет показан.",
"wayToGetPagerDutyKey": "Вы можете это получить, перейдя в Сервис -> Каталог сервисов -> (Выберите сервис) -> Интеграции -> Добавить интеграцию. Здесь вы можете искать «Events API V2». Подробнее {0}",
"apiKeyAddedMsg": "Ваш ключ API добавлен. Пожалуйста, обратите внимание на это сообщение, так как оно отображается один раз.",
"deleteAPIKeyMsg": "Вы уверены, что хотите удалить этот ключ API?",
"wayToGetPagerTreeIntegrationURL": "После создания интеграции Uptime Kuma в PagerTree, скопируйте конечную точку. Смотрите полную информацию {0}",
"wayToGetPagerTreeIntegrationURL": "После создания интеграции Uptime Kuma в PagerTree скопируйте файл Endpoint. См. полную информацию {0}",
"telegramMessageThreadIDDescription": "Необязательный уникальный идентификатор для цепочки сообщений (темы) форума; только для форумов-супергрупп",
"grpcMethodDescription": "Название метода - преобразовать в формат cammelCase, такой как sayHello, check и т.д.",
"Proto Service Name": "название службы Proto",
"grpcMethodDescription": "Имя метода преобразуется в формат cammelCase, например, sayHello, check и т. д.",
"Proto Service Name": "Название службы Proto",
"Proto Method": "Метод Proto",
"Proto Content": "Содержание Proto",
"telegramMessageThreadID": "(Необязательно) ID цепочки сообщений",
@ -758,5 +758,40 @@
"endDateTime": "Конечная дата и время",
"cronExpression": "Выражение для Cron",
"cronSchedule": "Расписание: ",
"invalidCronExpression": "Неверное выражение Cron: {0}"
"invalidCronExpression": "Неверное выражение Cron: {0}",
"ntfyUsernameAndPassword": "Логин и пароль",
"ntfyAuthenticationMethod": "Способ входа",
"Monitor Setting": "Настройка монитора {0}",
"Show Clickable Link": "Показать кликабельную ссылку",
"Badge Generator": "Генератор значков для {0}",
"Badge Type": "Тип значка",
"Badge Duration": "Срок действия значка",
"Badge Label": "Надпись для значка",
"Badge Prefix": "Префикс значка",
"Badge Label Color": "Цвет надписи значка",
"Badge Color": "Цвет значка",
"Badge Label Prefix": "Префикс надписи для значка",
"Open Badge Generator": "Открыть генератор значка",
"Badge Up Color": "Цвет значка для статуса \"Доступен\"",
"Badge Pending Color": "Цвет значка для статуса \"Ожидание\"",
"Badge Maintenance Color": "Цвет значка для статуса \"Техобслуживание\"",
"Badge Style": "Стиль значка",
"Badge Suffix": "Суффикс значка",
"Badge value (For Testing only.)": "Значение значка (только для тестирования)",
"Badge URL": "URL значка",
"Group": "Группа",
"Monitor Group": "Группа мониторов",
"Show Clickable Link Description": "Если флажок установлен, все, кто имеет доступ к этой странице состояния, могут иметь доступ к URL-адресу монитора.",
"pushoverMessageTtl": "TTL сообщения (в секундах)",
"Badge Down Color": "Цвет значка для статуса \"Недоступен\"",
"Badge Label Suffix": "Суффикс надписи для значка",
"Edit Maintenance": "Редактировать техобсоуживание",
"Reconnecting...": "Переподключение...",
"Cannot connect to the socket server": "Невозможно подключиться к серверу",
"Badge Warn Color": "Цвет значка для предупреждения",
"Badge Warn Days": "Значок для \"дней предупреждения\"",
"Badge Down Days": "Значок для \"дней недоступности\"",
"Home": "Главная",
"noGroupMonitorMsg": "Не доступно. Создайте сначала группу мониторов.",
"Close": "Закрыть"
}

View file

@ -214,7 +214,7 @@
"smtpBCC": "BCC",
"discord": "Discord",
"Discord Webhook URL": "Discord Webhook URL",
"wayToGetDiscordURL": "คุณสามารถรับได้โดยการไปที่ Server Settings -> Integrations -> Create Webhook",
"wayToGetDiscordURL": "คุณสามารถทำได้โดยการไปที่ Server Settings -> Integrations -> Create Webhook",
"Bot Display Name": "ชื่อบอท",
"Prefix Custom Message": "คำนำหน้าข้อความที่กำหนดเอง",
"Hello @everyone is...": "สวัสดี {'@'}everyone นี่…",
@ -652,5 +652,23 @@
"Enable DNS Cache": "เปิดใช้งาน DNS Cache",
"Enable": "เปิดใช้งาน",
"Disable": "ปิดใช้งาน",
"Single Maintenance Window": "หน้าการปรับปรุงเดี่ยว"
"Single Maintenance Window": "หน้าการปรับปรุงเดี่ยว",
"Clone Monitor": "มอนิเตอร์",
"Clone": "โคลนมอนิเตอร์",
"cloneOf": "ชื่อเล่นมอนิเตอร์",
"wayToGetZohoCliqURL": "คุณสามารถดูวิธีการสร้าง Webhook URL {0}",
"Cannot connect to the socket server": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ Socket",
"Reconnecting...": "กำลังเชื่อมต่อใหม่",
"Home": "หน้าหลัก",
"Date and Time": "วันที่และเวลา",
"DateTime Range": "ช่วงวันที่และเวลา",
"loadingError": "ไม่สามารถดึงข้อมูลได้ โปรดลองอีกครั้งในภายหลัง",
"plugin": "ปลั้กอิน | ปลั้กอิน",
"install": "ติดตั้ง",
"installing": "กำลังติดตั้ง",
"uninstall": "ถอนการติดตั้ง",
"uninstalling": "กำลังถอนการติดตั้ง",
"confirmUninstallPlugin": "แน่ใจหรือไม่ว่าต้องการถอนการติดตั้งปลั้กอินนี้?",
"Schedule Maintenance": "กำหนดเวลาซ่อมแซม",
"Edit Maintenance": "แก้ใขการบำรุงรักษา"
}

View file

@ -776,5 +776,13 @@
"Badge value (For Testing only.)": "Rozet değeri (Yalnızca Test için.)",
"Badge URL": "Rozet URL'i",
"Monitor Setting": "{0}'nin Monitör Ayarı",
"Show Clickable Link Description": "Eğer işaretlenirse, bu durum sayfasına erişimi olan herkes monitor URL'ine erişebilir."
"Show Clickable Link Description": "Eğer işaretlenirse, bu durum sayfasına erişimi olan herkes monitor URL'ine erişebilir.",
"Group": "Grup",
"Monitor Group": "Monitor Grup",
"Cannot connect to the socket server": "Soket sunucusuna bağlanılamıyor",
"Edit Maintenance": "Bakımı Düzenle",
"Reconnecting...": "Yeniden bağlanılıyor...",
"Home": "Anasayfa",
"noGroupMonitorMsg": "Uygun değil. Önce bir Grup Monitörü oluşturun.",
"Close": "Kapalı"
}

View file

@ -462,7 +462,7 @@
"onebotMessageType": "OneBot тип повідомлення",
"onebotGroupMessage": "Група",
"onebotPrivateMessage": "Приватне",
"onebotUserOrGroupId": "Група/Користувач ID",
"onebotUserOrGroupId": "Група/ID користувача",
"onebotSafetyTips": "Для безпеки необхідно встановити маркер доступу",
"PushDeer Key": "PushDeer ключ",
"Footer Text": "Текст нижнього колонтитула",
@ -782,5 +782,13 @@
"Badge Warn Color": "Колір бейджа \"Попередження\"",
"Badge Warn Days": "Бейдж \"Днів попередження\"",
"Badge Maintenance Color": "Колір бейджа \"Обслуговування\"",
"Badge Down Days": "Бейдж \"Днів недоступний\""
"Badge Down Days": "Бейдж \"Днів недоступний\"",
"Group": "Група",
"Monitor Group": "Група моніторів",
"Edit Maintenance": "Редагувати обслуговування",
"Cannot connect to the socket server": "Не вдається підключитися до сервера сокетів",
"Reconnecting...": "Повторне підключення...",
"Home": "Головна",
"noGroupMonitorMsg": "Недоступно. Спочатку створіть групу моніторів.",
"Close": "Закрити"
}

View file

@ -528,8 +528,8 @@
"RadiusCallingStationId": "呼叫方号码Calling Station Id",
"RadiusCallingStationIdDescription": "发出请求的设备的标识",
"Certificate Expiry Notification": "证书到期时通知",
"API Username": "API 用户名",
"API Key": "API 密钥",
"API Username": "API 用户名",
"API Key": "API 密钥",
"Recipient Number": "收件人手机号码",
"From Name/Number": "发件人名称/手机号码",
"Leave blank to use a shared sender number.": "留空以使用平台共享的发件人手机号码。",
@ -778,5 +778,13 @@
"Badge Label Prefix": "徽章标签前缀",
"Badge Label Color": "徽章标签颜色",
"Show Clickable Link Description": "勾选后所有能访问本状态页的访客均可查看该监控项网址。",
"Show Clickable Link": "显示可点击的监控项链接"
"Show Clickable Link": "显示可点击的监控项链接",
"Group": "组",
"Monitor Group": "监控项组",
"Cannot connect to the socket server": "无法连接到后端服务器",
"Reconnecting...": "重连中……",
"Edit Maintenance": "编辑维护计划",
"Home": "首页",
"noGroupMonitorMsg": "暂无可用,请先创建一个监控项组。",
"Close": "关闭"
}

View file

@ -706,5 +706,43 @@
"wayToGetKookBotToken": "到 {0} 創建應用程式並取得 bot token",
"dataRetentionTimeError": "保留期限必須為 0 或正數",
"infiniteRetention": "設定為 0 以作無限期保留。",
"confirmDeleteTagMsg": "你確定你要刪除此標籤?相關的監測器不會被刪除。"
"confirmDeleteTagMsg": "你確定你要刪除此標籤?相關的監測器不會被刪除。",
"twilioAuthToken": "認證 Token",
"twilioAccountSID": "帳號 SID",
"ntfyUsernameAndPassword": "使用者名稱和密碼",
"ntfyAuthenticationMethod": "認證類型",
"API Keys": "API 金鑰",
"Expiry": "到期",
"apiKey-inactive": "無效",
"apiKey-expired": "過期",
"Reconnecting...": "重新連線...",
"Expiry date": "到期時間",
"Don't expire": "不要過期",
"Continue": "繼續",
"Add Another": "新增作者",
"Add API Key": "新增 API 金鑰",
"Generate": "產生",
"lunaseaTarget": "目標",
"lunaseaDeviceID": "裝置 ID",
"lunaseaUserID": "使用者 ID",
"Cannot connect to the socket server": "無法連線到 Socket 伺服器",
"Edit Maintenance": "編輯維護",
"deleteAPIKeyMsg": "您確定要刪除這個 API 金鑰?",
"Custom Monitor Type": "自訂監視器類型",
"Google Analytics ID": "Google Analytics ID",
"Server Address": "伺服器位置",
"Edit Tag": "編輯標籤",
"pagertreeMedium": "中",
"pagertreeHigh": "高",
"pagertreeResolve": "自動解決",
"pagertreeLow": "低",
"Learn More": "閱讀更多",
"pushoverMessageTtl": "Message TTL (秒)",
"apiKeyAddedMsg": "您的 API 金鑰已建立。金鑰不會再次顯示,請將它放在安全的地方。",
"No API Keys": "無 API 金鑰",
"apiKey-active": "活躍",
"Expires": "過期",
"disableAPIKeyMsg": "您確定要停用這個 API 金鑰?",
"Monitor Setting": "{0} 的監視器設定",
"Guild ID": "Guild ID"
}

View file

@ -68,6 +68,7 @@
</div>
</div>
<!-- Stats -->
<div class="shadow-box big-padding text-center stats">
<div class="row">
<div v-if="monitor.type !== 'group'" class="col-12 col-sm col row d-flex align-items-center d-sm-block">
@ -131,6 +132,15 @@
</div>
</div>
<!-- Screenshot -->
<div v-if="monitor.type === 'real-browser'" class="shadow-box">
<div class="row">
<div class="col-md-6">
<img :src="screenshotURL" alt style="width: 100%;">
</div>
</div>
</div>
<div class="shadow-box table-shadow-box">
<div class="dropdown dropdown-clear-data">
<button class="btn btn-sm btn-outline-danger dropdown-toggle" type="button" data-bs-toggle="dropdown">
@ -217,6 +227,7 @@ import Tag from "../components/Tag.vue";
import CertificateInfo from "../components/CertificateInfo.vue";
import { getMonitorRelativeURL } from "../util.ts";
import { URL } from "whatwg-url";
import { getResBaseURL } from "../util-frontend";
export default {
components: {
@ -242,6 +253,7 @@ export default {
hideCount: true,
chunksNavigation: "scroll",
},
cacheTime: Date.now(),
};
},
computed: {
@ -251,6 +263,10 @@ export default {
},
lastHeartBeat() {
// Also trigger screenshot refresh here
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
this.cacheTime = Date.now();
if (this.monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[this.monitor.id]) {
return this.$root.lastHeartbeatList[this.monitor.id];
}
@ -325,11 +341,16 @@ export default {
pushURL() {
return this.$root.baseURL + "/api/push/" + this.monitor.pushToken + "?status=up&msg=OK&ping=";
},
screenshotURL() {
return getResBaseURL() + this.monitor.screenshot + "?time=" + this.cacheTime;
}
},
mounted() {
},
methods: {
getResBaseURL,
/** Request a test notification be sent for this monitor */
testNotification() {
this.$root.getSocket().emit("testNotification", this.monitor.id);

View file

@ -36,6 +36,10 @@
<option value="docker">
{{ $t("Docker Container") }}
</option>
<option value="real-browser">
HTTP(s) - Browser Engine (Chrome/Chromium) (Beta)
</option>
</optgroup>
<optgroup :label="$t('Passive Monitor Type')">
@ -73,16 +77,6 @@
Redis
</option>
</optgroup>
<!--
Hidden for now: Reason refer to Setting.vue
<optgroup :label="$t('Custom Monitor Type')">
<option value="browser">
(Beta) HTTP(s) - Browser Engine (Chrome/Firefox)
</option>
</optgroup>
</select>
-->
</select>
</div>
@ -103,7 +97,7 @@
</div>
<!-- URL -->
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'browser' " class="my-3">
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'real-browser' " class="my-3">
<label for="url" class="form-label">{{ $t("URL") }}</label>
<input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required>
</div>