mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-09-10 07:04:50 +08:00
Compare commits
58 Commits
louislam-p
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
ed3538f72f | ||
|
c6a029a895 | ||
|
c6048d56b4 | ||
|
0e30c43a80 | ||
|
cd9f83c54f | ||
|
3ce73684b5 | ||
|
e79be2af22 | ||
|
567f9b2b30 | ||
|
ffa041f8f2 | ||
|
f191699e63 | ||
|
470600df5b | ||
|
e8509e9904 | ||
|
9744f2a027 | ||
|
15a9b3d46f | ||
|
7b0bb866f7 | ||
|
027191503d | ||
|
7cedf5a417 | ||
|
9c816cd3cd | ||
|
4246290eed | ||
|
cc67be8b4b | ||
|
91681c7fce | ||
|
13917111fd | ||
|
69f1a73873 | ||
|
4c6f8d3e3b | ||
|
85455a1ebc | ||
|
a4d2e077b8 | ||
|
668636c9d5 | ||
|
8d3649966a | ||
|
bc2db2e36e | ||
|
7587269b62 | ||
|
4f944cd869 | ||
|
f027ce309e | ||
|
4b5ff08cdd | ||
|
e4baa99088 | ||
|
b9ac9fbb08 | ||
|
771d05363e | ||
|
d073d1642f | ||
|
20a11846d6 | ||
|
d360ce808d | ||
|
3c84abb3fd | ||
|
01e1edb545 | ||
|
102d70d8a7 | ||
|
8191f49b6c | ||
|
29ec447cc8 | ||
|
2f5ca5aa19 | ||
|
5a387538dc | ||
|
2e0299b76a | ||
|
2ec9fcca6d | ||
|
0713d44d37 | ||
|
bdf40835cc | ||
|
c1adcfbfc2 | ||
|
2a6d9b4acd | ||
|
2fd4e1cc72 | ||
|
7c88a38df3 | ||
|
d490285a44 | ||
|
5bbbef5305 | ||
|
487cb8fdc5 | ||
|
03037e2a9a |
36
.github/workflows/pr-reply.yml
vendored
36
.github/workflows/pr-reply.yml
vendored
@@ -1,36 +0,0 @@
|
||||
# Replys a message to all new PRs
|
||||
# The message:
|
||||
# - Say hello and thanks to the contributor
|
||||
# - Mention maintainers will review the PR soon
|
||||
# - To other people, show the testing pr command: npx kuma-pr <username:branch>
|
||||
# - Also show the advanced usage link: https://github.com/louislam/uptime-kuma/wiki/Test-Pull-Requests
|
||||
name: Reply to PRs
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
reply:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Reply to PR
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const pr = context.payload.pull_request;
|
||||
const message = `Hello @${pr.user.login}, thank you for your contribution! :tada:\n` +
|
||||
`The maintainers will review your PR soon.\n\n` +
|
||||
`If anyone would like to help test this PR, you can use the command:\n` +
|
||||
`\`\`\`bash\nnpx kuma-pr ${pr.user.login}:${pr.head.ref}\n\`\`\`\n\n` +
|
||||
`<sub> For advanced usage, please refer to our [wiki](https://github.com/louislam/uptime-kuma/wiki/Test-Pull-Requests) </sub>`;
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: pr.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: message
|
||||
});
|
@@ -0,0 +1,34 @@
|
||||
// Add column last_start_date to maintenance table
|
||||
exports.up = async function (knex) {
|
||||
await knex.schema
|
||||
.alterTable("maintenance", function (table) {
|
||||
table.datetime("last_start_date");
|
||||
});
|
||||
|
||||
// Perform migration for recurring-interval strategy
|
||||
const recurringMaintenances = await knex("maintenance").where({
|
||||
strategy: "recurring-interval",
|
||||
cron: "* * * * *"
|
||||
}).select("id", "start_time");
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
const maintenanceUpdates = recurringMaintenances.map(async ({ start_time, id }) => {
|
||||
// eslint-disable-next-line camelcase
|
||||
const [ hourStr, minuteStr ] = start_time.split(":");
|
||||
const hour = parseInt(hourStr, 10);
|
||||
const minute = parseInt(minuteStr, 10);
|
||||
|
||||
const cron = `${minute} ${hour} * * *`;
|
||||
|
||||
await knex("maintenance")
|
||||
.where({ id })
|
||||
.update({ cron });
|
||||
});
|
||||
await Promise.all(maintenanceUpdates);
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.alterTable("maintenance", function (table) {
|
||||
table.dropColumn("last_start_date");
|
||||
});
|
||||
};
|
15
db/knex_migrations/2025-07-17-0000-mqtt-websocket-path.js
Normal file
15
db/knex_migrations/2025-07-17-0000-mqtt-websocket-path.js
Normal file
@@ -0,0 +1,15 @@
|
||||
exports.up = function (knex) {
|
||||
// Add new column monitor.mqtt_websocket_path
|
||||
return knex.schema
|
||||
.alterTable("monitor", function (table) {
|
||||
table.string("mqtt_websocket_path", 255).nullable();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
// Drop column monitor.mqtt_websocket_path
|
||||
return knex.schema
|
||||
.alterTable("monitor", function (table) {
|
||||
table.dropColumn("mqtt_websocket_path");
|
||||
});
|
||||
};
|
@@ -47,9 +47,9 @@ RUN apt update && \
|
||||
|
||||
# Install cloudflared
|
||||
RUN curl https://pkg.cloudflare.com/cloudflare-main.gpg --output /usr/share/keyrings/cloudflare-main.gpg && \
|
||||
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared bullseye main' | tee /etc/apt/sources.list.d/cloudflared.list && \
|
||||
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared bookworm main' | tee /etc/apt/sources.list.d/cloudflared.list && \
|
||||
apt update && \
|
||||
apt install --yes --no-install-recommends -t stable cloudflared && \
|
||||
apt install --yes --no-install-recommends cloudflared && \
|
||||
cloudflared version && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
apt --yes autoremove
|
||||
|
@@ -24,9 +24,7 @@ if (! exists) {
|
||||
// Also update package-lock.json
|
||||
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
|
||||
childProcess.spawnSync(npm, [ "install" ]);
|
||||
|
||||
commit(version);
|
||||
tag(version);
|
||||
|
||||
} else {
|
||||
console.log("version tag exists, please delete the tag or use another tag");
|
||||
@@ -54,19 +52,6 @@ function commit(version) {
|
||||
console.log(res.stdout.toString().trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a tag with the specified version
|
||||
* @param {string} version Tag to create
|
||||
* @returns {void}
|
||||
*/
|
||||
function tag(version) {
|
||||
let res = childProcess.spawnSync("git", [ "tag", version ]);
|
||||
console.log(res.stdout.toString().trim());
|
||||
|
||||
res = childProcess.spawnSync("git", [ "push", "origin", version ]);
|
||||
console.log(res.stdout.toString().trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tag exists for the specified version
|
||||
* @param {string} version Version to check
|
||||
|
201
extra/generate-changelog.mjs
Normal file
201
extra/generate-changelog.mjs
Normal file
@@ -0,0 +1,201 @@
|
||||
// Script to generate changelog
|
||||
// Usage: node generate-changelog.mjs <previous-version-tag>
|
||||
// GitHub CLI (gh command) is required
|
||||
|
||||
import * as childProcess from "child_process";
|
||||
|
||||
const ignoreList = [
|
||||
"louislam",
|
||||
"CommanderStorm",
|
||||
"UptimeKumaBot",
|
||||
"weblate",
|
||||
"Copilot"
|
||||
];
|
||||
|
||||
const mergeList = [
|
||||
"Translations Update from Weblate",
|
||||
"Update dependencies",
|
||||
];
|
||||
|
||||
const template = `
|
||||
|
||||
LLM Task: Please help to put above PRs into the following sections based on their content. If a PR fits multiple sections, choose the most relevant one. If a PR doesn't fit any section, place it in "Others". If there are grammatical errors in the PR titles, please correct them. Don't change the PR numbers and authors, and keep the format. Output as markdown.
|
||||
|
||||
Changelog:
|
||||
|
||||
### 🆕 New Features
|
||||
|
||||
### 💇♀️ Improvements
|
||||
|
||||
### 🐞 Bug Fixes
|
||||
|
||||
### ⬆️ Security Fixes
|
||||
|
||||
### 🦎 Translation Contributions
|
||||
|
||||
### Others
|
||||
- Other small changes, code refactoring and comment/doc updates in this repo:
|
||||
`;
|
||||
|
||||
await main();
|
||||
|
||||
/**
|
||||
* Main Function
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function main() {
|
||||
const previousVersion = process.argv[2];
|
||||
|
||||
if (!previousVersion) {
|
||||
console.error("Please provide the previous version as the first argument.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Generating changelog since version ${previousVersion}...`);
|
||||
|
||||
try {
|
||||
const prList = await getPullRequestList(previousVersion);
|
||||
const list = [];
|
||||
|
||||
let i = 1;
|
||||
for (const pr of prList) {
|
||||
console.log(`Progress: ${i++}/${prList.length}`);
|
||||
let authorSet = await getAuthorList(pr.number);
|
||||
authorSet = await mainAuthorToFront(pr.author.login, authorSet);
|
||||
|
||||
if (mergeList.includes(pr.title)) {
|
||||
// Check if it is already in the list
|
||||
const existingItem = list.find(item => item.title === pr.title);
|
||||
if (existingItem) {
|
||||
existingItem.numbers.push(pr.number);
|
||||
for (const author of authorSet) {
|
||||
existingItem.authors.add(author);
|
||||
// Sort the authors
|
||||
existingItem.authors = new Set([ ...existingItem.authors ].sort((a, b) => a.localeCompare(b)));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const item = {
|
||||
numbers: [ pr.number ],
|
||||
title: pr.title,
|
||||
authors: authorSet,
|
||||
};
|
||||
|
||||
list.push(item);
|
||||
}
|
||||
|
||||
for (const item of list) {
|
||||
// Concat pr numbers into a string like #123 #456
|
||||
const prPart = item.numbers.map(num => `#${num}`).join(" ");
|
||||
|
||||
// Concat authors into a string like @user1 @user2
|
||||
let authorPart = [ ...item.authors ].map(author => `@${author}`).join(" ");
|
||||
|
||||
if (authorPart) {
|
||||
authorPart = `(Thanks ${authorPart})`;
|
||||
}
|
||||
|
||||
console.log(`- ${prPart} ${item.title} ${authorPart}`);
|
||||
}
|
||||
|
||||
console.log(template);
|
||||
|
||||
} catch (e) {
|
||||
console.error("Failed to get pull request list:", e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} previousVersion Previous Version Tag
|
||||
* @returns {Promise<object>} List of Pull Requests merged since previousVersion
|
||||
*/
|
||||
async function getPullRequestList(previousVersion) {
|
||||
// Get the date of previousVersion in YYYY-MM-DD format from git
|
||||
const previousVersionDate = childProcess.execSync(`git log -1 --format=%cd --date=short ${previousVersion}`).toString().trim();
|
||||
|
||||
if (!previousVersionDate) {
|
||||
throw new Error(`Unable to find the date of version ${previousVersion}. Please make sure the version tag exists.`);
|
||||
}
|
||||
|
||||
const ghProcess = childProcess.spawnSync("gh", [
|
||||
"pr",
|
||||
"list",
|
||||
"--state",
|
||||
"merged",
|
||||
"--base",
|
||||
"master",
|
||||
"--search",
|
||||
`merged:>=${previousVersionDate}`,
|
||||
"--json",
|
||||
"number,title,author",
|
||||
"--limit",
|
||||
"1000"
|
||||
], {
|
||||
encoding: "utf-8"
|
||||
});
|
||||
|
||||
if (ghProcess.error) {
|
||||
throw ghProcess.error;
|
||||
}
|
||||
|
||||
if (ghProcess.status !== 0) {
|
||||
throw new Error(`gh command failed with status ${ghProcess.status}: ${ghProcess.stderr}`);
|
||||
}
|
||||
|
||||
return JSON.parse(ghProcess.stdout);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} prID Pull Request ID
|
||||
* @returns {Promise<Set<string>>} Set of Authors' GitHub Usernames
|
||||
*/
|
||||
async function getAuthorList(prID) {
|
||||
const ghProcess = childProcess.spawnSync("gh", [
|
||||
"pr",
|
||||
"view",
|
||||
prID,
|
||||
"--json",
|
||||
"commits"
|
||||
], {
|
||||
encoding: "utf-8"
|
||||
});
|
||||
|
||||
if (ghProcess.error) {
|
||||
throw ghProcess.error;
|
||||
}
|
||||
|
||||
if (ghProcess.status !== 0) {
|
||||
throw new Error(`gh command failed with status ${ghProcess.status}: ${ghProcess.stderr}`);
|
||||
}
|
||||
|
||||
const prInfo = JSON.parse(ghProcess.stdout);
|
||||
const commits = prInfo.commits;
|
||||
|
||||
const set = new Set();
|
||||
|
||||
for (const commit of commits) {
|
||||
for (const author of commit.authors) {
|
||||
if (author.login && !ignoreList.includes(author.login)) {
|
||||
set.add(author.login);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the set
|
||||
return new Set([ ...set ].sort((a, b) => a.localeCompare(b)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} mainAuthor Main Author
|
||||
* @param {Set<string>} authorSet Set of Authors
|
||||
* @returns {Set<string>} New Set with mainAuthor at the front
|
||||
*/
|
||||
async function mainAuthorToFront(mainAuthor, authorSet) {
|
||||
if (ignoreList.includes(mainAuthor)) {
|
||||
return authorSet;
|
||||
}
|
||||
return new Set([ mainAuthor, ...authorSet ]);
|
||||
}
|
@@ -1,44 +0,0 @@
|
||||
// Generate on GitHub
|
||||
const input = `
|
||||
* Add Korean translation by @Alanimdeo in https://github.com/louislam/dockge/pull/86
|
||||
`;
|
||||
|
||||
const template = `
|
||||
### 🆕 New Features
|
||||
|
||||
### 💇♀️ Improvements
|
||||
|
||||
### 🐞 Bug Fixes
|
||||
|
||||
### ⬆️ Security Fixes
|
||||
|
||||
### 🦎 Translation Contributions
|
||||
|
||||
### Others
|
||||
- Other small changes, code refactoring and comment/doc updates in this repo:
|
||||
`;
|
||||
|
||||
const lines = input.split("\n").filter((line) => line.trim() !== "");
|
||||
|
||||
for (const line of lines) {
|
||||
// Split the last " by "
|
||||
const usernamePullRequesURL = line.split(" by ").pop();
|
||||
|
||||
if (!usernamePullRequesURL) {
|
||||
console.log("Unable to parse", line);
|
||||
continue;
|
||||
}
|
||||
|
||||
const [ username, pullRequestURL ] = usernamePullRequesURL.split(" in ");
|
||||
const pullRequestID = "#" + pullRequestURL.split("/").pop();
|
||||
let message = line.split(" by ").shift();
|
||||
|
||||
if (!message) {
|
||||
console.log("Unable to parse", line);
|
||||
continue;
|
||||
}
|
||||
|
||||
message = message.split("* ").pop();
|
||||
console.log("-", pullRequestID, message, `(Thanks ${username})`);
|
||||
}
|
||||
console.log(template);
|
@@ -8,7 +8,7 @@ import {
|
||||
checkVersionFormat,
|
||||
getRepoNames,
|
||||
pressAnyKey,
|
||||
execSync, uploadArtifacts,
|
||||
execSync, uploadArtifacts, checkReleaseBranch,
|
||||
} from "./lib.mjs";
|
||||
import semver from "semver";
|
||||
|
||||
@@ -23,6 +23,9 @@ if (!githubToken) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check if the current branch is "release"
|
||||
checkReleaseBranch();
|
||||
|
||||
// Check if the version is a valid semver
|
||||
checkVersionFormat(version);
|
||||
|
||||
|
@@ -7,7 +7,7 @@ import {
|
||||
checkTagExists,
|
||||
checkVersionFormat,
|
||||
getRepoNames,
|
||||
pressAnyKey, execSync, uploadArtifacts
|
||||
pressAnyKey, execSync, uploadArtifacts, checkReleaseBranch
|
||||
} from "./lib.mjs";
|
||||
|
||||
const repoNames = getRepoNames();
|
||||
@@ -21,6 +21,9 @@ if (!githubToken) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check if the current branch is "release"
|
||||
checkReleaseBranch();
|
||||
|
||||
// Check if the version is a valid semver
|
||||
checkVersionFormat(version);
|
||||
|
||||
|
@@ -249,3 +249,16 @@ export function execSync(cmd) {
|
||||
console.info(`[DRY RUN] ${cmd}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current branch is "release"
|
||||
* @returns {void}
|
||||
*/
|
||||
export function checkReleaseBranch() {
|
||||
const res = childProcess.spawnSync("git", [ "rev-parse", "--abbrev-ref", "HEAD" ]);
|
||||
const branch = res.stdout.toString().trim();
|
||||
if (branch !== "release") {
|
||||
console.error(`Current branch is ${branch}, please switch to "release" branch`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
@@ -28,9 +28,7 @@ if (! exists) {
|
||||
// Also update package-lock.json
|
||||
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
|
||||
childProcess.spawnSync(npm, [ "install" ]);
|
||||
|
||||
commit(newVersion);
|
||||
tag(newVersion);
|
||||
|
||||
} else {
|
||||
console.log("version exists");
|
||||
@@ -54,16 +52,6 @@ function commit(version) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a tag with the specified version
|
||||
* @param {string} version Tag to create
|
||||
* @returns {void}
|
||||
*/
|
||||
function tag(version) {
|
||||
let res = childProcess.spawnSync("git", [ "tag", version ]);
|
||||
console.log(res.stdout.toString().trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tag exists for the specified version
|
||||
* @param {string} version Version to check
|
||||
|
2210
package-lock.json
generated
2210
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "uptime-kuma",
|
||||
"version": "2.0.0-beta.3",
|
||||
"version": "2.0.0-beta.4",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -64,7 +64,8 @@
|
||||
"quick-run-nightly": "docker run --rm --env NODE_ENV=development -p 3001:3001 louislam/uptime-kuma:nightly2",
|
||||
"start-dev-container": "cd docker && docker-compose -f docker-compose-dev.yml up --force-recreate",
|
||||
"rebase-pr-to-1.23.X": "node extra/rebase-pr.js 1.23.X",
|
||||
"reset-migrate-aggregate-table-state": "node extra/reset-migrate-aggregate-table-state.js"
|
||||
"reset-migrate-aggregate-table-state": "node extra/reset-migrate-aggregate-table-state.js",
|
||||
"generate-changelog": "node ./extra/generate-changelog.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "~1.8.22",
|
||||
@@ -99,6 +100,7 @@
|
||||
"http-proxy-agent": "~7.0.2",
|
||||
"https-proxy-agent": "~7.0.6",
|
||||
"iconv-lite": "~0.6.3",
|
||||
"is-url": "^1.2.4",
|
||||
"isomorphic-ws": "^5.0.0",
|
||||
"jsesc": "~3.0.2",
|
||||
"jsonata": "^2.0.3",
|
||||
@@ -137,6 +139,7 @@
|
||||
"socket.io": "~4.8.0",
|
||||
"socket.io-client": "~4.8.0",
|
||||
"socks-proxy-agent": "~8.0.5",
|
||||
"sqlstring": "~2.3.3",
|
||||
"tar": "~6.2.1",
|
||||
"tcp-ping": "~0.1.1",
|
||||
"thirty-two": "~1.0.2",
|
||||
|
@@ -12,6 +12,7 @@ const { UptimeCalculator } = require("./uptime-calculator");
|
||||
const dayjs = require("dayjs");
|
||||
const { SimpleMigrationServer } = require("./utils/simple-migration-server");
|
||||
const KumaColumnCompiler = require("./utils/knex/lib/dialects/mysql2/schema/mysql2-columncompiler");
|
||||
const SqlString = require("sqlstring");
|
||||
|
||||
/**
|
||||
* Database & App Data Folder
|
||||
@@ -256,10 +257,6 @@ class Database {
|
||||
}
|
||||
};
|
||||
} else if (dbConfig.type === "mariadb") {
|
||||
if (!/^\w+$/.test(dbConfig.dbName)) {
|
||||
throw Error("Invalid database name. A database name can only consist of letters, numbers and underscores");
|
||||
}
|
||||
|
||||
const connection = await mysql.createConnection({
|
||||
host: dbConfig.hostname,
|
||||
port: dbConfig.port,
|
||||
@@ -267,7 +264,11 @@ class Database {
|
||||
password: dbConfig.password,
|
||||
});
|
||||
|
||||
await connection.execute("CREATE DATABASE IF NOT EXISTS " + dbConfig.dbName + " CHARACTER SET utf8mb4");
|
||||
// Set to true, so for example "uptime.kuma", becomes `uptime.kuma`, not `uptime`.`kuma`
|
||||
// Doc: https://github.com/mysqljs/sqlstring?tab=readme-ov-file#escaping-query-identifiers
|
||||
const escapedDBName = SqlString.escapeId(dbConfig.dbName, true);
|
||||
|
||||
await connection.execute("CREATE DATABASE IF NOT EXISTS " + escapedDBName + " CHARACTER SET utf8mb4");
|
||||
connection.end();
|
||||
|
||||
config = {
|
||||
|
@@ -202,7 +202,7 @@ class Maintenance extends BeanModel {
|
||||
* @returns {void}
|
||||
*/
|
||||
static validateCron(cron) {
|
||||
let job = new Cron(cron, () => {});
|
||||
let job = new Cron(cron, () => { });
|
||||
job.stop();
|
||||
}
|
||||
|
||||
@@ -239,6 +239,8 @@ class Maintenance extends BeanModel {
|
||||
apicache.clear();
|
||||
});
|
||||
} else if (this.cron != null) {
|
||||
let current = dayjs();
|
||||
|
||||
// Here should be cron or recurring
|
||||
try {
|
||||
this.beanMeta.status = "scheduled";
|
||||
@@ -258,6 +260,10 @@ class Maintenance extends BeanModel {
|
||||
this.beanMeta.status = "scheduled";
|
||||
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
|
||||
}, duration);
|
||||
|
||||
// Set last start date to current time
|
||||
this.last_start_date = current.toISOString();
|
||||
R.store(this);
|
||||
};
|
||||
|
||||
// Create Cron
|
||||
@@ -268,9 +274,25 @@ class Maintenance extends BeanModel {
|
||||
const startDateTime = startDate.hour(hour).minute(minute);
|
||||
this.beanMeta.job = new Cron(this.cron, {
|
||||
timezone: await this.getTimezone(),
|
||||
interval: this.interval_day * 24 * 60 * 60,
|
||||
startAt: startDateTime.toISOString(),
|
||||
}, startEvent);
|
||||
}, () => {
|
||||
if (!this.lastStartDate || this.interval_day === 1) {
|
||||
return startEvent();
|
||||
}
|
||||
|
||||
// If last start date is set, it means the maintenance has been started before
|
||||
let lastStartDate = dayjs(this.lastStartDate)
|
||||
.subtract(1.1, "hour"); // Subtract 1.1 hour to avoid issues with timezone differences
|
||||
|
||||
// Check if the interval is enough
|
||||
if (current.diff(lastStartDate, "day") < this.interval_day) {
|
||||
log.debug("maintenance", "Maintenance id: " + this.id + " is still in the window, skipping start event");
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("maintenance", "Maintenance id: " + this.id + " is not in the window, starting event");
|
||||
return startEvent();
|
||||
});
|
||||
} else {
|
||||
this.beanMeta.job = new Cron(this.cron, {
|
||||
timezone: await this.getTimezone(),
|
||||
@@ -279,7 +301,6 @@ class Maintenance extends BeanModel {
|
||||
|
||||
// Continue if the maintenance is still in the window
|
||||
let runningTimeslot = this.getRunningTimeslot();
|
||||
let current = dayjs();
|
||||
|
||||
if (runningTimeslot) {
|
||||
let duration = dayjs(runningTimeslot.endDate).diff(current, "second") * 1000;
|
||||
@@ -423,8 +444,11 @@ class Maintenance extends BeanModel {
|
||||
} else if (!this.strategy.startsWith("recurring-")) {
|
||||
this.cron = "";
|
||||
} else if (this.strategy === "recurring-interval") {
|
||||
// For intervals, the pattern is calculated in the run function as the interval-option is set
|
||||
this.cron = "* * * * *";
|
||||
// For intervals, the pattern is used to check if the execution should be started
|
||||
let array = this.start_time.split(":");
|
||||
let hour = parseInt(array[0]);
|
||||
let minute = parseInt(array[1]);
|
||||
this.cron = `${minute} ${hour} * * *`;
|
||||
this.duration = this.calcDuration();
|
||||
log.debug("maintenance", "Cron: " + this.cron);
|
||||
log.debug("maintenance", "Duration: " + this.duration);
|
||||
|
@@ -190,6 +190,7 @@ class Monitor extends BeanModel {
|
||||
radiusSecret: this.radiusSecret,
|
||||
mqttUsername: this.mqttUsername,
|
||||
mqttPassword: this.mqttPassword,
|
||||
mqttWebsocketPath: this.mqttWebsocketPath,
|
||||
authWorkstation: this.authWorkstation,
|
||||
authDomain: this.authDomain,
|
||||
tlsCa: this.tlsCa,
|
||||
@@ -1314,7 +1315,7 @@ class Monitor extends BeanModel {
|
||||
/**
|
||||
* Send a notification about a monitor
|
||||
* @param {boolean} isFirstBeat Is this beat the first of this monitor?
|
||||
* @param {Monitor} monitor The monitor to send a notificaton about
|
||||
* @param {Monitor} monitor The monitor to send a notification about
|
||||
* @param {Bean} bean Status information about monitor
|
||||
* @returns {void}
|
||||
*/
|
||||
|
@@ -15,6 +15,7 @@ class MqttMonitorType extends MonitorType {
|
||||
username: monitor.mqttUsername,
|
||||
password: monitor.mqttPassword,
|
||||
interval: monitor.interval,
|
||||
websocketPath: monitor.mqttWebsocketPath,
|
||||
});
|
||||
|
||||
if (monitor.mqttCheckType == null || monitor.mqttCheckType === "") {
|
||||
@@ -52,12 +53,12 @@ class MqttMonitorType extends MonitorType {
|
||||
* @param {string} hostname Hostname / address of machine to test
|
||||
* @param {string} topic MQTT topic
|
||||
* @param {object} options MQTT options. Contains port, username,
|
||||
* password and interval (interval defaults to 20)
|
||||
* password, websocketPath and interval (interval defaults to 20)
|
||||
* @returns {Promise<string>} Received MQTT message
|
||||
*/
|
||||
mqttAsync(hostname, topic, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { port, username, password, interval = 20 } = options;
|
||||
const { port, username, password, websocketPath, interval = 20 } = options;
|
||||
|
||||
// Adds MQTT protocol to the hostname if not already present
|
||||
if (!/^(?:http|mqtt|ws)s?:\/\//.test(hostname)) {
|
||||
@@ -70,7 +71,15 @@ class MqttMonitorType extends MonitorType {
|
||||
reject(new Error("Timeout, Message not received"));
|
||||
}, interval * 1000 * 0.8);
|
||||
|
||||
const mqttUrl = `${hostname}:${port}`;
|
||||
// Construct the URL based on protocol
|
||||
let mqttUrl = `${hostname}:${port}`;
|
||||
if (hostname.startsWith("ws://") || hostname.startsWith("wss://")) {
|
||||
if (websocketPath && !websocketPath.startsWith("/")) {
|
||||
mqttUrl = `${hostname}:${port}/${websocketPath || ""}`;
|
||||
} else {
|
||||
mqttUrl = `${hostname}:${port}${websocketPath || ""}`;
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("mqtt", `MQTT connecting to ${mqttUrl}`);
|
||||
|
||||
|
@@ -14,6 +14,18 @@ class GoogleChat extends NotificationProvider {
|
||||
|
||||
try {
|
||||
// Google Chat message formatting: https://developers.google.com/chat/api/guides/message-formats/basic
|
||||
if (notification.googleChatUseTemplate && notification.googleChatTemplate) {
|
||||
// Send message using template
|
||||
const renderedText = await this.renderTemplate(
|
||||
notification.googleChatTemplate,
|
||||
msg,
|
||||
monitorJSON,
|
||||
heartbeatJSON
|
||||
);
|
||||
const data = { "text": renderedText };
|
||||
await axios.post(notification.googleChatWebhookURL, data);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
let chatHeader = {
|
||||
title: "Uptime Kuma Alert",
|
||||
@@ -88,7 +100,6 @@ class GoogleChat extends NotificationProvider {
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -15,7 +15,7 @@ class PromoSMS extends NotificationProvider {
|
||||
notification.promosmsAllowLongSMS = false;
|
||||
}
|
||||
|
||||
//TODO: Add option for enabling special characters. It will decrese message max length from 160 to 70 chars.
|
||||
//TODO: Add option for enabling special characters. It will decrease message max length from 160 to 70 chars.
|
||||
//Lets remove non ascii char
|
||||
let cleanMsg = msg.replace(/[^\x00-\x7F]/g, "");
|
||||
|
||||
|
@@ -2,6 +2,7 @@ const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { setSettings, setting } = require("../util-server");
|
||||
const { getMonitorRelativeURL, UP, log } = require("../../src/util");
|
||||
const isUrl = require("is-url");
|
||||
|
||||
class Slack extends NotificationProvider {
|
||||
name = "slack";
|
||||
@@ -49,7 +50,7 @@ class Slack extends NotificationProvider {
|
||||
}
|
||||
|
||||
const address = this.extractAddress(monitorJSON);
|
||||
if (address) {
|
||||
if (isUrl(address)) {
|
||||
try {
|
||||
actions.push({
|
||||
"type": "button",
|
||||
|
@@ -720,6 +720,17 @@ let needSetup = false;
|
||||
|
||||
monitor.rabbitmqNodes = JSON.stringify(monitor.rabbitmqNodes);
|
||||
|
||||
/*
|
||||
* List of frontend-only properties that should not be saved to the database.
|
||||
* Should clean up before saving to the database.
|
||||
*/
|
||||
const frontendOnlyProperties = [ "humanReadableInterval" ];
|
||||
for (const prop of frontendOnlyProperties) {
|
||||
if (prop in monitor) {
|
||||
delete monitor[prop];
|
||||
}
|
||||
}
|
||||
|
||||
bean.import(monitor);
|
||||
bean.user_id = socket.userID;
|
||||
|
||||
@@ -837,6 +848,7 @@ let needSetup = false;
|
||||
bean.mqttTopic = monitor.mqttTopic;
|
||||
bean.mqttSuccessMessage = monitor.mqttSuccessMessage;
|
||||
bean.mqttCheckType = monitor.mqttCheckType;
|
||||
bean.mqttWebsocketPath = monitor.mqttWebsocketPath;
|
||||
bean.databaseConnectionString = monitor.databaseConnectionString;
|
||||
bean.databaseQuery = monitor.databaseQuery;
|
||||
bean.authMethod = monitor.authMethod;
|
||||
|
@@ -208,11 +208,13 @@ class SetupDatabase {
|
||||
|
||||
// Test connection
|
||||
try {
|
||||
log.info("setup-database", "Testing database connection...");
|
||||
const connection = await mysql.createConnection({
|
||||
host: dbConfig.hostname,
|
||||
port: dbConfig.port,
|
||||
user: dbConfig.username,
|
||||
password: dbConfig.password,
|
||||
database: dbConfig.dbName,
|
||||
});
|
||||
await connection.execute("SELECT 1");
|
||||
connection.end();
|
||||
|
@@ -27,7 +27,7 @@ module.exports.apiKeySocketHandler = (socket) => {
|
||||
log.debug("apikeys", "Added API Key");
|
||||
log.debug("apikeys", key);
|
||||
|
||||
// Append key ID and prefix to start of key seperated by _, used to get
|
||||
// Append key ID and prefix to start of key separated by _, used to get
|
||||
// correct hash when validating key.
|
||||
let formattedKey = "uk" + bean.id + "_" + clearKey;
|
||||
await sendAPIKeyList(socket);
|
||||
|
@@ -343,6 +343,8 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
statusPageID
|
||||
]);
|
||||
|
||||
apicache.clear();
|
||||
|
||||
} else {
|
||||
throw new Error("Status Page is not found");
|
||||
}
|
||||
|
@@ -582,7 +582,7 @@ class UptimeCalculator {
|
||||
let totalPing = 0;
|
||||
let endTimestamp;
|
||||
|
||||
// Get the eariest timestamp of the required period based on the type
|
||||
// Get the earliest timestamp of the required period based on the type
|
||||
switch (type) {
|
||||
case "day":
|
||||
endTimestamp = key - 86400 * (num - 1);
|
||||
@@ -710,7 +710,7 @@ class UptimeCalculator {
|
||||
|
||||
let endTimestamp;
|
||||
|
||||
// Get the eariest timestamp of the required period based on the type
|
||||
// Get the earliest timestamp of the required period based on the type
|
||||
switch (type) {
|
||||
case "day":
|
||||
endTimestamp = key - 86400 * (num - 1);
|
||||
|
@@ -482,6 +482,7 @@ optgroup {
|
||||
.info {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
@@ -7,11 +7,17 @@
|
||||
class="beat-hover-area"
|
||||
:class="{ 'empty': (beat === 0) }"
|
||||
:style="beatHoverAreaStyle"
|
||||
:title="getBeatTitle(beat)"
|
||||
:aria-label="getBeatAriaLabel(beat)"
|
||||
role="status"
|
||||
tabindex="0"
|
||||
@mouseenter="showTooltip(beat, $event)"
|
||||
@mouseleave="hideTooltip"
|
||||
@focus="showTooltip(beat, $event)"
|
||||
@blur="hideTooltip"
|
||||
>
|
||||
<div
|
||||
class="beat"
|
||||
:class="{ 'empty': (beat === 0), 'down': (beat.status === 0), 'pending': (beat.status === 2), 'maintenance': (beat.status === 3) }"
|
||||
:class="getBeatClasses(beat)"
|
||||
:style="beatStyle"
|
||||
/>
|
||||
</div>
|
||||
@@ -24,13 +30,27 @@
|
||||
<div v-if="$root.styleElapsedTime === 'with-line'" class="connecting-line"></div>
|
||||
<div>{{ timeSinceLastBeat }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Tooltip -->
|
||||
<Tooltip
|
||||
:visible="tooltipVisible"
|
||||
:content="tooltipContent"
|
||||
:x="tooltipX"
|
||||
:y="tooltipY"
|
||||
:position="tooltipPosition"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import dayjs from "dayjs";
|
||||
import { DOWN, UP, PENDING, MAINTENANCE } from "../util.ts";
|
||||
import Tooltip from "./Tooltip.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Tooltip,
|
||||
},
|
||||
props: {
|
||||
/** Size of the heartbeat bar */
|
||||
size: {
|
||||
@@ -46,6 +66,11 @@ export default {
|
||||
heartbeatList: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
/** Heartbeat bar days */
|
||||
heartbeatBarDays: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -56,10 +81,25 @@ export default {
|
||||
beatHoverAreaPadding: 4,
|
||||
move: false,
|
||||
maxBeat: -1,
|
||||
// Tooltip data
|
||||
tooltipVisible: false,
|
||||
tooltipContent: null,
|
||||
tooltipX: 0,
|
||||
tooltipY: 0,
|
||||
tooltipPosition: "below",
|
||||
tooltipTimeoutId: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
||||
/**
|
||||
* Normalized heartbeatBarDays as a number
|
||||
* @returns {number} Number of days for heartbeat bar
|
||||
*/
|
||||
normalizedHeartbeatBarDays() {
|
||||
return Math.max(0, Math.min(365, Math.floor(this.heartbeatBarDays || 0)));
|
||||
},
|
||||
|
||||
/**
|
||||
* If heartbeatList is null, get it from $root.heartbeatList
|
||||
* @returns {object} Heartbeat list
|
||||
@@ -80,6 +120,12 @@ export default {
|
||||
if (!this.beatList) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// For configured ranges, no padding needed since we show all beats
|
||||
if (this.normalizedHeartbeatBarDays > 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let num = this.beatList.length - this.maxBeat;
|
||||
|
||||
if (this.move) {
|
||||
@@ -98,8 +144,20 @@ export default {
|
||||
return [];
|
||||
}
|
||||
|
||||
// If heartbeat days is configured (not auto), data is already aggregated from server
|
||||
if (this.normalizedHeartbeatBarDays > 0 && this.beatList.length > 0) {
|
||||
// Show all beats from server - they are already properly aggregated
|
||||
return this.beatList;
|
||||
}
|
||||
|
||||
// Original logic for auto mode (heartbeatBarDays = 0)
|
||||
let placeholders = [];
|
||||
|
||||
// Handle case where maxBeat is -1 (no limit)
|
||||
if (this.maxBeat <= 0) {
|
||||
return this.beatList;
|
||||
}
|
||||
|
||||
let start = this.beatList.length - this.maxBeat;
|
||||
|
||||
if (this.move) {
|
||||
@@ -172,13 +230,17 @@ export default {
|
||||
* @returns {string} The time elapsed in minutes or hours.
|
||||
*/
|
||||
timeSinceFirstBeat() {
|
||||
if (this.normalizedHeartbeatBarDays === 1) {
|
||||
return (this.normalizedHeartbeatBarDays * 24) + "h";
|
||||
}
|
||||
if (this.normalizedHeartbeatBarDays >= 2) {
|
||||
return this.normalizedHeartbeatBarDays + "d";
|
||||
}
|
||||
|
||||
// Need to calculate from actual data
|
||||
const firstValidBeat = this.shortBeatList.at(this.numPadding);
|
||||
const minutes = dayjs().diff(dayjs.utc(firstValidBeat?.time), "minutes");
|
||||
if (minutes > 60) {
|
||||
return (minutes / 60).toFixed(0) + "h";
|
||||
} else {
|
||||
return minutes + "m";
|
||||
}
|
||||
return minutes > 60 ? Math.floor(minutes / 60) + "h" : minutes + "m";
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -197,15 +259,15 @@ export default {
|
||||
if (seconds < tolerance) {
|
||||
return this.$t("now");
|
||||
} else if (seconds < 60 * 60) {
|
||||
return this.$t("time ago", [ (seconds / 60).toFixed(0) + "m" ]);
|
||||
return this.$t("time ago", [ (seconds / 60).toFixed(0) + "m" ] );
|
||||
} else {
|
||||
return this.$t("time ago", [ (seconds / 60 / 60).toFixed(0) + "h" ]);
|
||||
return this.$t("time ago", [ (seconds / 60 / 60).toFixed(0) + "h" ] );
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
beatList: {
|
||||
handler(val, oldVal) {
|
||||
handler() {
|
||||
this.move = true;
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -217,6 +279,10 @@ export default {
|
||||
},
|
||||
unmounted() {
|
||||
window.removeEventListener("resize", this.resize);
|
||||
// Clean up tooltip timeout
|
||||
if (this.tooltipTimeoutId) {
|
||||
clearTimeout(this.tooltipTimeoutId);
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
if (this.heartbeatList === null) {
|
||||
@@ -256,7 +322,23 @@ export default {
|
||||
*/
|
||||
resize() {
|
||||
if (this.$refs.wrap) {
|
||||
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatHoverAreaPadding * 2));
|
||||
const newMaxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatHoverAreaPadding * 2));
|
||||
|
||||
// If maxBeat changed and we're in configured days mode, notify parent to reload data
|
||||
if (newMaxBeat !== this.maxBeat && this.normalizedHeartbeatBarDays > 0) {
|
||||
this.maxBeat = newMaxBeat;
|
||||
|
||||
// Find the closest parent with reloadHeartbeatData method (StatusPage)
|
||||
let parent = this.$parent;
|
||||
while (parent && !parent.reloadHeartbeatData) {
|
||||
parent = parent.$parent;
|
||||
}
|
||||
if (parent && parent.reloadHeartbeatData) {
|
||||
parent.reloadHeartbeatData(newMaxBeat);
|
||||
}
|
||||
} else {
|
||||
this.maxBeat = newMaxBeat;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -267,7 +349,124 @@ export default {
|
||||
* @returns {string} Beat title
|
||||
*/
|
||||
getBeatTitle(beat) {
|
||||
return `${this.$root.datetime(beat.time)}` + ((beat.msg) ? ` - ${beat.msg}` : "");
|
||||
if (!beat) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Show timestamp for all beats (both individual and aggregated)
|
||||
return `${this.$root.datetime(beat.time)}${beat.msg ? ` - ${beat.msg}` : ""}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get CSS classes for a beat element based on its status
|
||||
* @param {object} beat - Beat object containing status information
|
||||
* @returns {object} Object with CSS class names as keys and boolean values
|
||||
*/
|
||||
getBeatClasses(beat) {
|
||||
if (beat === 0 || beat === null || beat?.status === null) {
|
||||
return { empty: true };
|
||||
}
|
||||
|
||||
const status = Number(beat.status);
|
||||
|
||||
return {
|
||||
down: status === DOWN,
|
||||
pending: status === PENDING,
|
||||
maintenance: status === MAINTENANCE
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the aria-label for accessibility
|
||||
* @param {object} beat Beat to get aria-label from
|
||||
* @returns {string} Aria label
|
||||
*/
|
||||
getBeatAriaLabel(beat) {
|
||||
switch (beat?.status) {
|
||||
case DOWN:
|
||||
return `Down at ${this.$root.datetime(beat.time)}`;
|
||||
case UP:
|
||||
return `Up at ${this.$root.datetime(beat.time)}`;
|
||||
case PENDING:
|
||||
return `Pending at ${this.$root.datetime(beat.time)}`;
|
||||
case MAINTENANCE:
|
||||
return `Maintenance at ${this.$root.datetime(beat.time)}`;
|
||||
default:
|
||||
return "No data";
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Show custom tooltip
|
||||
* @param {object} beat Beat data
|
||||
* @param {Event} event Mouse event
|
||||
* @returns {void}
|
||||
*/
|
||||
showTooltip(beat, event) {
|
||||
if (beat === 0 || !beat) {
|
||||
this.hideTooltip();
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear any existing timeout
|
||||
if (this.tooltipTimeoutId) {
|
||||
clearTimeout(this.tooltipTimeoutId);
|
||||
}
|
||||
|
||||
// Small delay for better UX
|
||||
this.tooltipTimeoutId = setTimeout(() => {
|
||||
this.tooltipContent = beat;
|
||||
|
||||
// Calculate position relative to viewport
|
||||
const rect = event.target.getBoundingClientRect();
|
||||
|
||||
// Position relative to viewport
|
||||
const x = rect.left + (rect.width / 2);
|
||||
const y = rect.top;
|
||||
|
||||
// Check if tooltip would go off-screen and adjust position
|
||||
const tooltipHeight = 80; // Approximate tooltip height
|
||||
const viewportHeight = window.innerHeight;
|
||||
const spaceAbove = y;
|
||||
const spaceBelow = viewportHeight - y - rect.height;
|
||||
|
||||
if (spaceAbove > tooltipHeight && spaceBelow < tooltipHeight) {
|
||||
// Show above - arrow points down
|
||||
this.tooltipPosition = "above";
|
||||
this.tooltipY = y - 10;
|
||||
} else {
|
||||
// Show below - arrow points up
|
||||
this.tooltipPosition = "below";
|
||||
this.tooltipY = y + rect.height + 10;
|
||||
}
|
||||
|
||||
// Ensure tooltip doesn't go off the left or right edge
|
||||
const tooltipWidth = 120; // Approximate tooltip width
|
||||
let adjustedX = x;
|
||||
|
||||
if ((x - tooltipWidth / 2) < 10) {
|
||||
adjustedX = tooltipWidth / 2 + 10;
|
||||
} else if ((x + tooltipWidth / 2) > (window.innerWidth - 10)) {
|
||||
adjustedX = window.innerWidth - tooltipWidth / 2 - 10;
|
||||
}
|
||||
|
||||
this.tooltipX = adjustedX;
|
||||
this.tooltipVisible = true;
|
||||
}, 150);
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide custom tooltip
|
||||
* @returns {void}
|
||||
*/
|
||||
hideTooltip() {
|
||||
if (this.tooltipTimeoutId) {
|
||||
clearTimeout(this.tooltipTimeoutId);
|
||||
this.tooltipTimeoutId = null;
|
||||
}
|
||||
|
||||
this.tooltipVisible = false;
|
||||
this.tooltipContent = null;
|
||||
},
|
||||
|
||||
},
|
||||
|
@@ -182,7 +182,7 @@ export default {
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (newPeriod == "0") {
|
||||
this.heartbeatList = null;
|
||||
this.$root.storage().removeItem(`chart-period-${this.monitorId}`);
|
||||
this.$root.storage()["chart-period"] = newPeriod;
|
||||
} else {
|
||||
this.loading = true;
|
||||
|
||||
@@ -199,7 +199,7 @@ export default {
|
||||
this.$root.toastError(res.msg);
|
||||
} else {
|
||||
this.chartRawData = res.data;
|
||||
this.$root.storage()[`chart-period-${this.monitorId}`] = newPeriod;
|
||||
this.$root.storage()["chart-period"] = newPeriod;
|
||||
}
|
||||
this.loading = false;
|
||||
});
|
||||
@@ -216,7 +216,7 @@ export default {
|
||||
},
|
||||
created() {
|
||||
// Load chart period from storage if saved
|
||||
let period = this.$root.storage()[`chart-period-${this.monitorId}`];
|
||||
let period = this.$root.storage()["chart-period"];
|
||||
if (period != null) {
|
||||
// Has this ever been not a string?
|
||||
if (typeof period !== "string") {
|
||||
@@ -224,7 +224,7 @@ export default {
|
||||
}
|
||||
this.chartPeriodHrs = period;
|
||||
} else {
|
||||
this.chartPeriodHrs = "24";
|
||||
this.chartPeriodHrs = "0";
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
|
276
src/components/Tooltip.vue
Normal file
276
src/components/Tooltip.vue
Normal file
@@ -0,0 +1,276 @@
|
||||
<template>
|
||||
<teleport to="body">
|
||||
<div
|
||||
v-if="content"
|
||||
ref="tooltip"
|
||||
class="tooltip-wrapper"
|
||||
:style="tooltipStyle"
|
||||
:class="{ 'tooltip-above': position === 'above' }"
|
||||
>
|
||||
<div class="tooltip-content">
|
||||
<slot :content="content">
|
||||
<!-- Default content if no slot provided -->
|
||||
<div class="tooltip-status" :class="statusClass">
|
||||
{{ statusText }}
|
||||
</div>
|
||||
<div class="tooltip-time">{{ timeText }}</div>
|
||||
<div v-if="content?.msg" class="tooltip-message">{{ content.msg }}</div>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="tooltip-arrow" :class="{ 'arrow-above': position === 'above' }"></div>
|
||||
</div>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DOWN, UP, PENDING, MAINTENANCE } from "../util.ts";
|
||||
|
||||
export default {
|
||||
name: "Tooltip",
|
||||
props: {
|
||||
/** Whether tooltip is visible */
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/** Content object to display */
|
||||
content: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
/** X position (viewport coordinates) */
|
||||
x: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
/** Y position (viewport coordinates) */
|
||||
y: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
/** Position relative to target element */
|
||||
position: {
|
||||
type: String,
|
||||
default: "below",
|
||||
validator: (value) => [ "above", "below" ].includes(value)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
tooltipStyle() {
|
||||
return {
|
||||
left: this.x + "px",
|
||||
top: this.y + "px",
|
||||
};
|
||||
},
|
||||
|
||||
statusText() {
|
||||
if (!this.content || this.content === 0) {
|
||||
return this.$t("Unknown");
|
||||
}
|
||||
|
||||
switch (this.content.status) {
|
||||
case DOWN:
|
||||
return this.$t("Down");
|
||||
case UP:
|
||||
return this.$t("Up");
|
||||
case PENDING:
|
||||
return this.$t("Pending");
|
||||
case MAINTENANCE:
|
||||
return this.$t("Maintenance");
|
||||
default:
|
||||
return this.$t("Unknown");
|
||||
}
|
||||
},
|
||||
|
||||
statusClass() {
|
||||
if (!this.content || this.content === 0) {
|
||||
return "status-empty";
|
||||
}
|
||||
|
||||
switch (this.content.status) {
|
||||
case DOWN:
|
||||
return "status-down";
|
||||
case UP:
|
||||
return "status-up";
|
||||
case PENDING:
|
||||
return "status-pending";
|
||||
case MAINTENANCE:
|
||||
return "status-maintenance";
|
||||
default:
|
||||
return "status-unknown";
|
||||
}
|
||||
},
|
||||
|
||||
timeText() {
|
||||
if (!this.content || this.content === 0) {
|
||||
return "";
|
||||
}
|
||||
return this.$root.datetime(this.content.time);
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.tooltip-wrapper {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
transform: translateX(-50%);
|
||||
|
||||
.tooltip-content {
|
||||
background: rgba(17, 24, 39, 0.95);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(75, 85, 99, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.25);
|
||||
min-width: 120px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 14px;
|
||||
height: 2px;
|
||||
background: rgba(17, 24, 39, 0.95);
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.tooltip-status {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
|
||||
&.status-up {
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
&.status-down {
|
||||
color: $danger;
|
||||
}
|
||||
|
||||
&.status-pending {
|
||||
color: $warning;
|
||||
}
|
||||
|
||||
&.status-maintenance {
|
||||
color: $maintenance;
|
||||
}
|
||||
|
||||
&.status-empty {
|
||||
color: $secondary-text;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-time {
|
||||
color: #d1d5db;
|
||||
font-size: 13px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.tooltip-message {
|
||||
color: #f3f4f6;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
padding-top: 4px;
|
||||
border-top: 1px solid rgba(75, 85, 99, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-arrow {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 12px;
|
||||
height: 6px;
|
||||
overflow: hidden;
|
||||
top: -6px;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 100%;
|
||||
transform: translateX(-50%) translateY(-50%) rotate(45deg);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: rgba(17, 24, 39, 0.95);
|
||||
border: 1px solid rgba(75, 85, 99, 0.3);
|
||||
border-bottom: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
&.arrow-above {
|
||||
top: auto;
|
||||
bottom: -6px;
|
||||
|
||||
&::before {
|
||||
top: 0%;
|
||||
transform: translateX(-50%) translateY(-50%) rotate(225deg);
|
||||
border: 1px solid rgba(75, 85, 99, 0.3);
|
||||
border-bottom: none;
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Smooth entrance animation
|
||||
animation: tooltip-fade-in 0.2s $easing-out;
|
||||
|
||||
&.tooltip-above {
|
||||
transform: translateX(-50%) translateY(-8px);
|
||||
|
||||
.tooltip-content::before {
|
||||
top: auto;
|
||||
bottom: -1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dark theme adjustments
|
||||
.dark .tooltip-wrapper {
|
||||
.tooltip-content {
|
||||
background: rgba(31, 41, 55, 0.95);
|
||||
border-color: rgba(107, 114, 128, 0.3);
|
||||
|
||||
&::before {
|
||||
background: rgba(31, 41, 55, 0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-arrow {
|
||||
&::before {
|
||||
background: rgba(31, 41, 55, 0.95);
|
||||
border-color: rgba(107, 114, 128, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tooltip-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(4px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Accessibility improvements
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.tooltip-wrapper {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -10,4 +10,40 @@
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input id="google-chat-use-template" v-model="$parent.notification.googleChatUseTemplate" type="checkbox" class="form-check-input">
|
||||
<label for="google-chat-use-template" class="form-check-label"> {{ $t("Template plain text instead of using cards") }} </label>
|
||||
<i18n-t tag="p" class="form-text" keypath="issueWithGoogleChatOnAndroidHelptext">
|
||||
<template #issuetackerURL>
|
||||
<a href="https://issuetracker.google.com/issues/283746283" target="_blank">issuetracker.google.com/issues/283746283</a>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="$parent.notification.googleChatUseTemplate">
|
||||
<div class="mb-3">
|
||||
<TemplatedTextarea id="google-chat-template" v-model="$parent.notification.googleChatTemplate" :required="true" :placeholder="googleChatTemplatePlaceholder" />
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TemplatedTextarea from "../TemplatedTextarea.vue";
|
||||
|
||||
export default {
|
||||
name: "GoogleChat",
|
||||
components: {
|
||||
TemplatedTextarea,
|
||||
},
|
||||
computed: {
|
||||
googleChatTemplatePlaceholder() {
|
||||
return this.$t("Example:", [
|
||||
"{{ name }} - {{ msg }}{% if hostnameOrURL %} ({{ hostnameOrURL }}){% endif %}"
|
||||
]);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@@ -18,7 +18,7 @@
|
||||
{{ $t("matrixDesc1") }}
|
||||
</p>
|
||||
<i18n-t tag="p" keypath="matrixDesc2" style="margin-top: 8px;">
|
||||
<code>curl -XPOST -d '{"type": "m.login.password", "identifier": {"user": "botusername", "type": "m.id.user"}, "password": "passwordforuser"}' "https://home.server/_matrix/client/v3/login"</code>.
|
||||
<code>curl -XPOST --json '{"type": "m.login.password", "identifier": {"user": "botusername", "type": "m.id.user"}, "password": "passwordforuser"}' "https://home.server/_matrix/client/v3/login"</code>.
|
||||
</i18n-t>
|
||||
</div>
|
||||
</template>
|
||||
|
@@ -47,6 +47,7 @@ const languageList = {
|
||||
"ge": "ქართული",
|
||||
"uz": "O'zbek tili",
|
||||
"ga": "Gaeilge",
|
||||
"sk": "Slovenčina",
|
||||
};
|
||||
|
||||
let messages = {
|
||||
|
@@ -1175,5 +1175,17 @@
|
||||
"ipFamilyDescriptionAutoSelect": "Използва {happyEyeballs} за определяне на IP семейството.",
|
||||
"Manual": "Ръковосдство",
|
||||
"OAuth Audience": "OAuth аудитория",
|
||||
"Optional: The audience to request the JWT for": "По желание: Аудиторията, за която да се поиска JWT"
|
||||
"Optional: The audience to request the JWT for": "По желание: Аудиторията, за която да се поиска JWT",
|
||||
"mqttWebSocketPath": "MQTT Уеб сокет път",
|
||||
"mqttWebsocketPathInvalid": "Моля, използвайте валиден формат на пътя за Уеб сокет",
|
||||
"Path": "Път",
|
||||
"mqttWebsocketPathExplanation": "Уеб сокет път за MQTT през Уеб сокет връзки (напр. /mqtt)",
|
||||
"mqttHostnameTip": "Моля, използвайте този формат {hostnameFormat}",
|
||||
"issueWithGoogleChatOnAndroidHelptext": "Това позволява да се заобиколят грешки, като например {issuetackerURL}",
|
||||
"Template plain text instead of using cards": "Шаблон с обикновен текст вместо използване на карти",
|
||||
"Events cleared successfully": "Събитията са изчистени успешно.",
|
||||
"No monitors found": "Не са намерени монитори.",
|
||||
"Clear All Events": "Изчисти всички събития",
|
||||
"clearAllEventsMsg": "Сигурни ли сте, че желатет да изчистите всички събития?",
|
||||
"Could not clear events": "Неуспешен опит за изчистване на {failed}/{total} събития"
|
||||
}
|
||||
|
@@ -569,5 +569,50 @@
|
||||
"Messaging API": "API de missatges",
|
||||
"wayToGetLineChannelToken": "Primer accediu a {0}, creeu un proveïdor i un canal (API de missatgeria) i, a continuació, podeu obtenir el token d'accés al canal i l'identificador d'usuari dels elements del menú esmentats anteriorment.",
|
||||
"Icon URL": "URL de la icona",
|
||||
"aboutIconURL": "Pots donar un enllaç a la imatge a \"URL de la icona\" per sobreposar-la a la imatge de perfil pere defecte. No s'usarà si hi ha una icona d'emoji establerta."
|
||||
"aboutIconURL": "Pots donar un enllaç a la imatge a \"URL de la icona\" per sobreposar-la a la imatge de perfil pere defecte. No s'usarà si hi ha una icona d'emoji establerta.",
|
||||
"tagAlreadyOnMonitor": "Aquesta etiqueta (nom i valor) ja hi és al monitor o està pendent d'afegir.",
|
||||
"mqttWebsocketPathInvalid": "Si us plau, feu servir un format de directori de WebSocket vàlid",
|
||||
"confirmDeleteTagMsg": "Estàs segur que vols eliminar aquesta etiqueta? Els monitors associats amb aquesta etiqueta no seran eliminats.",
|
||||
"To Phone Number": "Al número de telèfon",
|
||||
"Manual": "Manual",
|
||||
"aboutMattermostChannelName": "Podeu substituir el canal predeterminat al qual publica el Webhook introduint el nom del canal al camp \"Nom del canal\". Això s'ha d'activar a la configuració de Mattermost Webhook. Ex: #altre-canal",
|
||||
"enableGRPCTls": "Permet enviar una sol·licitud gRPC amb connexió TLS",
|
||||
"deleteMaintenanceMsg": "N'esteu segur que voleu suprimir aquest manteniment?",
|
||||
"dnsPortDescription": "Port del servidor DNS. El valor predeterminat és 53. Podeu canviar el port en qualsevol moment.",
|
||||
"enableDefaultNotificationDescription": "Aquesta notificació s'habilitarà per defecte per als monitors nous. Encara podeu desactivar la notificació per separat per a cada monitor.",
|
||||
"importHandleDescription": "Trieu \"Omet l'existent\" si voleu ometre tots els monitors o notificacions amb el mateix nom. \"Sobreescriure\" suprimirà tots els monitors i notificacions existents.",
|
||||
"affectedStatusPages": "Mostra aquest missatge de manteniment a les pàgines d'estat seleccionades",
|
||||
"Path": "Directori",
|
||||
"mqttWebSocketPath": "Directori MQTT WebSocket",
|
||||
"mqttHostnameTip": "Si us plau, feu servir aquest format {hostnameFormat}",
|
||||
"mqttWebsocketPathExplanation": "Directori de WebSocket per a MQTT a través de connexions WebSocket (p. ex., /mqtt)",
|
||||
"Use HTML for custom E-mail body": "Utilitzeu HTML per al cos personalitzat del correu electrònic",
|
||||
"dataRetentionTimeError": "El període de retenció ha de ser 0 o superior",
|
||||
"infiniteRetention": "Establiu a 0 per a una retenció infinita.",
|
||||
"grpcMethodDescription": "El nom del mètode es converteix en format camelCase, com ara sayHello, check, etc.",
|
||||
"acceptedStatusCodesDescription": "Seleccioneu els codis d'estat que es considerin una resposta satisfactòria.",
|
||||
"deleteMonitorMsg": "N'esteu segur que voleu suprimir aquest monitor?",
|
||||
"deleteNotificationMsg": "Confirmes que vols suprimir aquesta notificació per a tots els monitors?",
|
||||
"resolverserverDescription": "Cloudflare és el servidor predeterminat. Podeu canviar el servidor de resolució en qualsevol moment.",
|
||||
"rrtypeDescription": "Seleccioneu el tipus de RR que voleu controlar",
|
||||
"pauseMonitorMsg": "Segur que vols pausar-ho?",
|
||||
"clearEventsMsg": "Confirmes que vols suprimir tots els esdeveniments d'aquest monitor?",
|
||||
"clearHeartbeatsMsg": "Confirmes que vols suprimir tots els batecs d'aquest monitor?",
|
||||
"confirmClearStatisticsMsg": "Esteu segur que voleu suprimir TOTES les estadístiques?",
|
||||
"confirmImportMsg": "N'esteu segur que voleu importar la còpia de seguretat? Verifiqueu que heu seleccionat l'opció d'importació correcta.",
|
||||
"twoFAVerifyLabel": "Introduïu el vostre token per verificar 2FA:",
|
||||
"tokenValidSettingsMsg": "El token és vàlid! Ara podeu desar la configuració de 2FA.",
|
||||
"confirmEnableTwoFAMsg": "N'esteu segur que voleu activar 2FA?",
|
||||
"confirmDisableTwoFAMsg": "Esteu segur que voleu deshabilitar 2FA?",
|
||||
"recurringIntervalMessage": "Executar un cop cada dia | Executeu un cop cada {0} dies",
|
||||
"affectedMonitorsDescription": "Seleccioneu els monitors afectats pel manteniment actual",
|
||||
"atLeastOneMonitor": "Seleccioneu almenys un monitor afectat",
|
||||
"passwordNotMatchMsg": "La contrasenya repetida no coincideix.",
|
||||
"notificationDescription": "Les notificacions s'han d'assignar a un monitor perquè funcionin.",
|
||||
"pushyToken": "Token de dispositiu",
|
||||
"Add Tags": "Afegir etiquetes",
|
||||
"tagAlreadyStaged": "Aquesta etiqueta (nom i valor) ja està preparada per a aquest lot.",
|
||||
"tagNameExists": "Ja existeix una etiqueta del sistema amb aquest nom. Seleccioneu-la de la llista o utilitzeu un nom diferent.",
|
||||
"high": "alt",
|
||||
"defaultFriendlyName": "Nou monitor"
|
||||
}
|
||||
|
@@ -400,8 +400,8 @@
|
||||
"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)",
|
||||
"smseagleEncoding": "Odeslat v Unicode (standardně=GSM-7)",
|
||||
"smseaglePriority": "Priorita zprávy (0-9, nejvyšší priorita = 9)",
|
||||
"stackfield": "Stackfield",
|
||||
"Customize": "Přizpůsobit",
|
||||
"Custom Footer": "Vlastní patička",
|
||||
@@ -828,10 +828,10 @@
|
||||
"showCertificateExpiry": "Zobrazit vypršení platnosti certifikátu",
|
||||
"pushDeerServerDescription": "Chcete-li používat oficiální server, ponechte prázdné",
|
||||
"noOrBadCertificate": "Žádný/Vadný certifikát",
|
||||
"nostrRelays": "Relé Nostr",
|
||||
"nostrRelays": "Nostr relay",
|
||||
"FlashDuty Severity": "Závažnost",
|
||||
"PushDeer Server": "Server PushDeer",
|
||||
"wayToGetFlashDutyKey": "Můžete přejít na stránku \"Kanál -> (Vyberte kanál) -> Integrace -> Přidat novou integraci\", přidat \"Uptime Kuma\" a získat push adresu, zkopírovat integrační klíč v adrese. Další informace naleznete na adrese",
|
||||
"wayToGetFlashDutyKey": "Pro integrování Uptime Kuma s Flashduty: přejděte do sekce Kanály > Vyberte kanál > Integrace > Přidat novou integraci\", vyberte možnost Uptime Kuma a zkopírujte Push adresu.",
|
||||
"Request Timeout": "Časový limit požadavku",
|
||||
"timeoutAfter": "Vypršení časového limitu po {0} sekundách",
|
||||
"styleElapsedTime": "Čas uplynulý pod heartbeat ukazatelem",
|
||||
@@ -1091,5 +1091,89 @@
|
||||
"rabbitmqHelpText": "Abyste mohli používat tento monitor, musíte v nastavení RabbitMQ povolit modul pro správu. Další informace naleznete na adrese {rabitmq_documentation}.",
|
||||
"SendGrid API Key": "SendGrid API klíč",
|
||||
"Separate multiple email addresses with commas": "Více e-mailových adres oddělte čárkami",
|
||||
"Clear": "Odstranění"
|
||||
"Clear": "Odstranění",
|
||||
"Ip Family": "Rodina IP protokolů",
|
||||
"ipFamilyDescriptionAutoSelect": "Pro určení rodiny IP protokolů se využívá {happyEyeballs}.",
|
||||
"Clear Form": "Vymazat formulář",
|
||||
"Happy Eyeballs algorithm": "Happy Eyeballs algoritmus",
|
||||
"pause": "Pauza",
|
||||
"Manual": "Ručně",
|
||||
"Plain Text": "Prostý text",
|
||||
"pingGlobalTimeoutLabel": "Globální časový limit",
|
||||
"pingGlobalTimeoutDescription": "Celková doba v sekundách před zastavením pingu, bez ohledu na počet odeslaných paketů",
|
||||
"pingPerRequestTimeoutLabel": "Časový limit pingu",
|
||||
"pingIntervalAdjustedInfo": "Interval upravený na základě počtu paketů, globálního časového limitu a časového limitu pro ping",
|
||||
"Custom URL": "Vlastní URL",
|
||||
"customUrlDescription": "Použije se jako hypertextový odkaz místo adresy monitoru.",
|
||||
"OneChatAccessToken": "OneChat Přístupový token",
|
||||
"OneChatUserIdOrGroupId": "OneChat ID uživatele nebo ID skupiny",
|
||||
"OneChatBotId": "OneChat ID bota",
|
||||
"wahaSession": "Relace",
|
||||
"wahaChatId": "ID chatu (telefonní číslo / ID kontaktu / ID skupiny)",
|
||||
"wayToGetWahaApiUrl": "URL vaší WAHA instance.",
|
||||
"wayToGetWahaApiKey": "API klíč je hodnota proměnné prostředí WHATSAPP_API_KEY, kterou jste použili při spuštění WAHA.",
|
||||
"Message Template": "Šablony zprávy",
|
||||
"Template Format": "Formát zprávy",
|
||||
"smsplanetApiDocs": "Detailní informace týkající se získání API tokenů naleznete v {the_smsplanet_documentation}.",
|
||||
"the smsplanet documentation": "dokumentaci smsplanet",
|
||||
"Phone numbers": "Telefonní čísla",
|
||||
"Sender name": "Jméno odesílatele",
|
||||
"smsplanetNeedToApproveName": "Je vyžadováno schválení v klientském portále",
|
||||
"Disable URL in Notification": "Zakázat URL v oznámeních",
|
||||
"Add Another Tag": "Přidat další štítek",
|
||||
"Staged Tags for Batch Add": "Připravené štítky pro hromadné přidání",
|
||||
"ntfyPriorityDown": "Priorita pro DOWN-události",
|
||||
"pingNumericLabel": "Číselný výstup",
|
||||
"Font Twemoji by Twitter licensed under": "Písmo Twemoji od Twitteru je pod licencí",
|
||||
"smsplanetApiToken": "Token pro SMSPlanet API",
|
||||
"tagAlreadyStaged": "Tento štítek (název a hodnota) je již pro tuto dávku připraven.",
|
||||
"telegramUseTemplateDescription": "Po aktivování této možnosti bude při odeslání zprávy použita vlastní šablona.",
|
||||
"telegramServerUrlDescription": "Pro zrušení omezení api botů Telegramu nebo získání přístupu v blokovaných oblastech (Čína, Írán, atp.). Pro více informací klikněte na {0}. Výchozí nastavení: {1}",
|
||||
"Use HTML for custom E-mail body": "Použít HTML v těle vlastního e-mailu",
|
||||
"ntfyPriorityHelptextPriorityHigherThanDown": "Pravidelná priorita by měla být vyšší než priorita {0}. Priorita {1} je vyšší než {0} s prioritou {2}",
|
||||
"OAuth Audience": "OAuth Audience",
|
||||
"Optional: The audience to request the JWT for": "Volitelné: Audience, pro které se má vyžádat JWT",
|
||||
"pingCountDescription": "Počet paketů, které se před zastavením odešlou",
|
||||
"pingNumericDescription": "Pokud je tato možnost aktivní, místo symbolických názvů hostitelů se zobrazí IP adresy",
|
||||
"pingPerRequestTimeoutDescription": "Jedná se o maximální dobu čekání (v sekundách) před tím, než bude konkrétní ping paket považován za ztracený",
|
||||
"smtpHelpText": "Pomocí možnosti 'SMTPS' se ověří funkčnost protokolu SMTP/TLS; výběrem ‚Ignorovat TLS‘ se naváže něšifrované spojení; po vybrání 'STARTTLS‘ se odešle příkaz STARTTLS a dojde k ověření certifikátu serveru. Ani jeden z těchto režimů neinicializuje odeslání e-mailu.",
|
||||
"wayToGetWahaSession": "Z této relace WAHA odesílá oznámení do ID chatu. Najdete ho v nástěnce WAHA.",
|
||||
"wayToWriteWahaChatId": "Telefonní číslo s mezinárodní předvolbou, ale bez znaménka plus na začátku ({0}), ID kontaktu ({1}) nebo ID skupiny ({2}). Oznámení jsou z relace WAHA odesílána na toto ID chatu.",
|
||||
"telegramServerUrl": "(Volitelné) Adresa serveru",
|
||||
"YZJ Robot Token": "YZJ Robot token",
|
||||
"YZJ Webhook URL": "YZJ URL webhooku",
|
||||
"Add Tags": "Přidat štítky",
|
||||
"tagAlreadyOnMonitor": "Dohled již má přiřazen tento štítek (název a hodnota) nebo se čeká na jeho přidání.",
|
||||
"tagNameExists": "Systémový štítek s tímto názvem již existuje. Vyberte jej ze seznamu nebo použijte jiný název.",
|
||||
"telegramUseTemplate": "Použít vlastní šablonu zprávy",
|
||||
"telegramTemplateFormatDescription": "Telegram umožňuje ve zprávách používat různé značkovací jazyky, více informací naleznete v Telegram {0}.",
|
||||
"smseagleDocs": "Zkontrolujte dokumentaci nebo dostupnost APIv2: {0}",
|
||||
"smseagleComma": "Více záznamů oddělte čárkou",
|
||||
"SpugPush Template Code": "Kód šablony",
|
||||
"FlashDuty Push URL": "Push URL",
|
||||
"FlashDuty Push URL Placeholder": "Zkopírovat ze stránky integrace upozornění",
|
||||
"pingCountLabel": "Max. počet paketů",
|
||||
"templateServiceName": "název služby",
|
||||
"templateHostnameOrURL": "název hostitele nebo URL",
|
||||
"templateStatus": "stav",
|
||||
"defaultFriendlyName": "Nový dohled",
|
||||
"smseagleContactV2": "ID kontaktu z telefonního seznamu",
|
||||
"smseagleGroupV2": "ID skupin(y) telefonního seznamu",
|
||||
"smseagleMsgType": "Typ zprávy",
|
||||
"smseagleMsgSms": "SMS zprávy (výchozí)",
|
||||
"smseagleMsgRing": "Vyzvánění",
|
||||
"smseagleMsgTts": "Hovor s převodem textu na řeč",
|
||||
"smseagleMsgTtsAdvanced": "Rozšířený hovor s převodem textu na řeč",
|
||||
"smseagleDuration": "Doba (v sekundách)",
|
||||
"smseagleTtsModel": "ID modelu převodu textu na řeč",
|
||||
"smseagleApiType": "Verze API",
|
||||
"smseagleApiv1": "APIv1 (pro stávající projekty a zpětnou kompatibilitu)",
|
||||
"smseagleApiv2": "APIv2 (doporučeno pro nové integrace)",
|
||||
"Path": "Cesta",
|
||||
"mqttWebsocketPathInvalid": "Použijte, prosím platný formát WebSocket cesty",
|
||||
"mqttHostnameTip": "Použijte, prosím tento formát {hostnameFormat}",
|
||||
"mqttWebSocketPath": "MQTT WebSocket cesta",
|
||||
"mqttWebsocketPathExplanation": "WebSocket cesta pro MQTT prostřednictvím WebSocket spojení (např. /mqtt)",
|
||||
"Template plain text instead of using cards": "Šablona prostého textu namísto použití karet",
|
||||
"issueWithGoogleChatOnAndroidHelptext": "Tím se můžete rovněž vyhnout chybám v upstreamu jako je {issuetackerURL}"
|
||||
}
|
||||
|
@@ -798,5 +798,9 @@
|
||||
"Host URL": "Host URL",
|
||||
"Request Timeout": "Anmod Timeout",
|
||||
"Cannot connect to the socket server": "Kan ikke oprette forbindelse til socket serveren",
|
||||
"pushViewCode": "Hvordan bruger man Push Monitor? (Se kode)"
|
||||
"pushViewCode": "Hvordan bruger man Push Monitor? (Se kode)",
|
||||
"Path": "Sti",
|
||||
"Add Tags": "Tilføj Tags",
|
||||
"tagAlreadyOnMonitor": "Dette tag (navn og værdi) er allerde tilføjet til denne overvågning, eller afventer tilføjelse.",
|
||||
"defaultFriendlyName": "Ny Overvågning"
|
||||
}
|
||||
|
@@ -1172,5 +1172,17 @@
|
||||
"pause": "Pause",
|
||||
"Staged Tags for Batch Add": "Bereitgestellte Tags für Batch-Hinzufügen",
|
||||
"OAuth Audience": "OAuth Zielgruppe",
|
||||
"Optional: The audience to request the JWT for": "Optional: Die Zielgruppe, für die das JWT angefordert werden soll"
|
||||
"Optional: The audience to request the JWT for": "Optional: Die Zielgruppe, für die das JWT angefordert werden soll",
|
||||
"Path": "Pfad",
|
||||
"mqttWebSocketPath": "MQTT WebSocket Pfad",
|
||||
"mqttWebsocketPathExplanation": "WebSocket-Pfad für MQTT über WebSocket-Verbindungen (z. B. /mqtt)",
|
||||
"mqttWebsocketPathInvalid": "Verwende ein gültiges WebSocket-Pfadformat",
|
||||
"mqttHostnameTip": "Verwende dieses Format {hostnameFormat}",
|
||||
"Template plain text instead of using cards": "Textvorlage anstelle von Karten",
|
||||
"issueWithGoogleChatOnAndroidHelptext": "Dies ermöglicht auch die Umgehung von Fehlern im Vorfeld wie {issuetackerURL}",
|
||||
"Clear All Events": "Alle Ereignisse löschen",
|
||||
"clearAllEventsMsg": "Möchtest du wirklich alle Ereignisse löschen?",
|
||||
"Events cleared successfully": "Ereignisse erfolgreich gelöscht.",
|
||||
"No monitors found": "Keine Monitore gefunden.",
|
||||
"Could not clear events": "{failed}/{total} Ereignisse konnten nicht gelöscht werden"
|
||||
}
|
||||
|
@@ -1175,5 +1175,17 @@
|
||||
"Staged Tags for Batch Add": "Bereitgestellte Tags für Batch-Hinzufügen",
|
||||
"pause": "Pause",
|
||||
"OAuth Audience": "OAuth Zielgruppe",
|
||||
"Optional: The audience to request the JWT for": "Optional: Die Zielgruppe, für die das JWT angefordert werden soll"
|
||||
"Optional: The audience to request the JWT for": "Optional: Die Zielgruppe, für die das JWT angefordert werden soll",
|
||||
"mqttWebsocketPathInvalid": "Verwende ein gültiges WebSocket-Pfadformat",
|
||||
"Path": "Pfad",
|
||||
"mqttWebSocketPath": "MQTT WebSocket Pfad",
|
||||
"mqttWebsocketPathExplanation": "WebSocket-Pfad für MQTT über WebSocket-Verbindungen (z. B. /mqtt)",
|
||||
"mqttHostnameTip": "Verwende dieses Format {hostnameFormat}",
|
||||
"Template plain text instead of using cards": "Textvorlage anstelle von Karten",
|
||||
"issueWithGoogleChatOnAndroidHelptext": "Dies ermöglicht auch die Umgehung von Fehlern im Vorfeld wie {issuetackerURL}",
|
||||
"clearAllEventsMsg": "Möchtest du wirklich alle Ereignisse löschen?",
|
||||
"Events cleared successfully": "Ereignisse erfolgreich gelöscht.",
|
||||
"No monitors found": "Keine Monitore gefunden.",
|
||||
"Could not clear events": "{failed}/{total} Ereignisse konnten nicht gelöscht werden",
|
||||
"Clear All Events": "Alle Ereignisse löschen"
|
||||
}
|
||||
|
@@ -5,7 +5,7 @@
|
||||
"resendEveryXTimes": "Επανάληψη αποστολής ειδοποίησης κάθε {0} φορές",
|
||||
"resendDisabled": "Η επανάληψη αποστολής ειδοποίησης είναι απενεργοποιημένη",
|
||||
"retriesDescription": "Μέγιστες επαναλήψεις προτού η υπηρεσία επισημανθεί ως κατω και σταλεί μια ειδοποίηση",
|
||||
"ignoreTLSError": "Παράβλεψη σφάλματος TLS/SSL για ιστότοπους HTTPS",
|
||||
"ignoreTLSError": "Παράβλεψη σφαλμάτων TLS/SSL για ιστότοπους HTTPS",
|
||||
"upsideDownModeDescription": "Αναποδογυρίστε την κατάσταση. Εάν η υπηρεσία είναι προσβάσιμη, είναι ΚΑΤΩ.",
|
||||
"maxRedirectDescription": "Μέγιστος αριθμός redirect που θα ακολουθήσουν. Ρυθμίστε το 0 για να απενεργοποιήσετε τα redirect.",
|
||||
"acceptedStatusCodesDescription": "Επιλέξτε κωδικούς κατάστασης που θεωρούνται επιτυχή.",
|
||||
@@ -84,8 +84,8 @@
|
||||
"pushOptionalParams": "Προαιρετικές παράμετροι: {0}",
|
||||
"Save": "Αποθηκεύση",
|
||||
"Notifications": "Ειδοποιήσεις",
|
||||
"Not available, please setup.": "Μη διαθέσιμο, παρακαλώ ρυθμίστε.",
|
||||
"Setup Notification": "Δημιουργία ειδοποίησης",
|
||||
"Not available, please setup.": "Μη διαθέσιμο, παρακαλώ αρχικοποιήστε.",
|
||||
"Setup Notification": "Αρχικοποίηση ειδοποίησης",
|
||||
"Light": "Φωτεινό",
|
||||
"Dark": "Σκοτεινό",
|
||||
"Auto": "Αυτόματο",
|
||||
@@ -547,7 +547,7 @@
|
||||
"Query": "Query",
|
||||
"settingsCertificateExpiry": "Λήξη πιστοποιητικού TLS",
|
||||
"certificationExpiryDescription": "Οι παρακολουθήσεις HTTPS ενεργοποιούν ειδοποίηση όταν λήξει το πιστοποιητικό TLS σε:",
|
||||
"Setup Docker Host": "Ρύθμιση Docker Host",
|
||||
"Setup Docker Host": "Αρχικοποίηση Docker Host",
|
||||
"Connection Type": "Τύπος σύνδεσης",
|
||||
"Docker Daemon": "Docker Daemon",
|
||||
"deleteDockerHostMsg": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτόν τον κεντρικό υπολογιστή βάσης για όλες τις παρακολουθήσεις;",
|
||||
@@ -611,7 +611,7 @@
|
||||
"Affected Monitors": "Επηρεαζόμενοι Επόπτες",
|
||||
"Pick Affected Monitors...": "Διαλέξτε Επηρεαζόμενους Επόπτες…",
|
||||
"webhookAdditionalHeadersTitle": "Επιπρόσθετες Κεφαλίδες",
|
||||
"webhookAdditionalHeadersDesc": "Ορίζει επιπρόσθετες κεφαλίδες που θα σταλθούν με το webhook.",
|
||||
"webhookAdditionalHeadersDesc": "Ορίζει επιπρόσθετες κεφαλίδες που θα σταλθούν με το webhook. Κάθε κεφαλίδα πρέπει να ορίζεται ώς JSON ζεύγος κλειδιού/τιμής.",
|
||||
"weekdayShortSun": "Κυρ",
|
||||
"dayOfWeek": "Ημέρα της Εβδομάδας",
|
||||
"dayOfMonth": "Ημέρα του Μήνα",
|
||||
@@ -645,12 +645,12 @@
|
||||
"warningTimezone": "Χρησιμοποιεί την ζώνη ώρας του server",
|
||||
"squadcast": "Squadcast",
|
||||
"IconUrl": "URL εικονιδίου",
|
||||
"Enable DNS Cache": "Ενεργοποίηση DNS Cache",
|
||||
"Enable DNS Cache": "(Καταργήθηκε) Ενεργοποίηση DNS Cache για οθόνες HTTP(s)",
|
||||
"Enable": "Ενεργοποίηση",
|
||||
"Disable": "Απενεργοποίηση",
|
||||
"Single Maintenance Window": "Μονό Παράθυρο Συντήρησης",
|
||||
"Maintenance Time Window of a Day": "Ημερίσιο πρόγραμμα Συντήρησης",
|
||||
"Effective Date Range": "Ημερομηνιακό Διάστημα Εφαρμογής",
|
||||
"Effective Date Range": "Ημερομηνιακό Διάστημα Εφαρμογής (Προαιρετικό)",
|
||||
"Schedule Maintenance": "Προγραμματισμός Συντήρησης",
|
||||
"Date and Time": "Ημερομηνία και Ώρα",
|
||||
"DateTime Range": "Ημερομηνιακό Πλαίσιο",
|
||||
@@ -708,5 +708,84 @@
|
||||
"dbName": "Όνομα βάσης δεδομένων",
|
||||
"Invert Keyword": "Αντιστροφή Λέξης-Κλειδιού",
|
||||
"Expected Value": "Αναμενόμενη Τιμή",
|
||||
"Json Query": "Ερώτημα Json"
|
||||
"Json Query": "Ερώτημα Json",
|
||||
"Add Tags": "Πρόσθεσε ετικέτες",
|
||||
"tagAlreadyOnMonitor": "Η ετικέτα (όνομα και τιμή) υπάρχει ήδη στην Οθόνη ή αναμένει να προστεθεί.",
|
||||
"tagNameExists": "Μια ετικέτα συστήματος υπάρχει ήδη με αυτό το όνομα. Επιλέξτε την από τη λίστα ή χρησιμοποιήστε διαφορετικό όνομα.",
|
||||
"liquidIntroduction": "H ικανότητα προσαρμογής σε πρότυπο επιτυγχάνεται με τη γλώσσα προτύπων Liquid. Παρακαλώ βρείτε οδηγίες χρήσης στο {0}. Οι διαθέσιμες μεταβλητές είναι:",
|
||||
"mqttWebsocketPathExplanation": "WebSocket μονοπάτι για MQTT πάνω σε συνδέσεις WebSocket (π.χ., /mqtt)",
|
||||
"successKeyword": "Λέξη-κλειδί σε περίπτωση επιτυχίας",
|
||||
"successKeywordExplanation": "MQTT Λέξη-κλειδί που θα θεωρηθεί επιτυχία",
|
||||
"statusPageSpecialSlugDesc": "Ειδικό slug {0}: η σελίδα δεν θα φανεί αν δεν παρέχεται κανένα slug",
|
||||
"Add a new expiry notification day": "Προσθέστε νέα μέρα ειδοποίησης λήξης",
|
||||
"noDockerHostMsg": "Μη διαθέσιμο. Πρώτα αρχικοποιήστε εναν Docker Host.",
|
||||
"tailscalePingWarning": "Προκειμένου να χρησιμοποιήσετε την οθόνη Tailscale Ping, χρειάζεται να εγκαταστήσετε το Uptime Kuma χωρίς Docker και επίσης εναν Tailscale client στον server σας.",
|
||||
"telegramProtectContentDescription": "Αν είναι ενεργοποιημένο, τα μηνύματα από μποτς στο Telegram θα προστατεύονται από προώθηση και αποθήκευση.",
|
||||
"telegramUseTemplateDescription": "Αν είναι ενεργοποιημένο, το μηνυμα θα σταλεί με προσαρμοσμένο πρότυπο.",
|
||||
"telegramServerUrlDescription": "Για να υπερβείτε τους περιορισμούς του Bot API του Telegram ή να αποκτήσετε πρόσβαση σε αποκλεισμένες περιοχές (Κίνα, Ιραν, κλπ). Για περισσότερες πληροφορίες πατήστε {0}. Προεπιλογή: {1}",
|
||||
"invalidCronExpression": "Μη έγκυρη έκφραση Cron: {0}",
|
||||
"chromeExecutableDescription": "Για χρήστες Docker, αν το Chromium δεν είναι ακόμη εγκατεστημένο, μπορεί να πάρει μερικά λεπτά να εγκατασταθεί και να εμφανίσει το αποτέλεσμα της δοκιμής. Παίρνει 1GB χώρου στο δίσκο.",
|
||||
"telegramServerUrl": "(Προαιρετικό) Server Url",
|
||||
"Path": "Μονοπάτι",
|
||||
"styleElapsedTimeShowWithLine": "Εμφάνισε (Με Γραμμή)",
|
||||
"filterActive": "Ενεργό",
|
||||
"filterActivePaused": "Σε Διακοπή",
|
||||
"templateMsg": "μήνυμα ειδοποίησης",
|
||||
"templateHeartbeatJSON": "Αντικείμενο που περιγράφει το heartbeat",
|
||||
"templateMonitorJSON": "Αντικείμενο που περιγράφει την οθόνη",
|
||||
"templateLimitedToUpDownCertNotifications": "διαθέσιμο μόνο για ειδοποιήσεις τύπου UP/DOWN/Certificate expiry",
|
||||
"templateLimitedToUpDownNotifications": "Διαθέσιμο μόνο για ειδοποιήσεις τύπου UP/DOWN",
|
||||
"mqttWebSocketPath": "Μονοπάτι MQTT WebSocket",
|
||||
"mqttWebsocketPathInvalid": "Παρακαλώ χρησιμοποιείστε έγκυρη μορφή μονοπατιού WebSocket",
|
||||
"mqttHostnameTip": "Παρακαλώ χρησιμοποιείστε τη μορφή {hostnameFormat}",
|
||||
"Select": "Επιλέξτε",
|
||||
"selectedMonitorCount": "Επιλεγμένο: {0}",
|
||||
"Check/Uncheck": "Επιλέξτε/Απεπιλέξτε",
|
||||
"Remove the expiry notification": "Αφαιρέστε την ημέρα ειδοποίησης λήξης",
|
||||
"Refresh Interval": "Ανανεώστε το μεσοδιάστημα",
|
||||
"Refresh Interval Description": "Η σελίδα κατάστασης θα ανανεώνεται κάθε {0} δευτερόλεπτα",
|
||||
"DockerHostRequired": "Παρακαλώ ορίστε τον Docker Host για αυτή την οθόνη.",
|
||||
"telegramMessageThreadID": "(Προαιρετικό) Αναγνωριστικό Νήματος Μηνύματος",
|
||||
"telegramMessageThreadIDDescription": "Προαιρετικό μοναδικό αναγνωριστικό για τον νήμα (θέμα) που αναφέρεστε στο forum, μόνο για υπερσύνολα του forum",
|
||||
"telegramSendSilently": "Στείλτε αθόρυβα",
|
||||
"telegramProtectContent": "Προστατέψτε Προώθηση/Αποθήκευση",
|
||||
"telegramUseTemplate": "Χρησιμοποιήστε προσαρμοσμένο πρότυπο μηνύματος",
|
||||
"telegramTemplateFormatDescription": "Το Telegram επιτρέπει τη χρήση διαφορετικών γλωσσών markup για μηνύματα, βλέπε Telegram {0} για συγκεκριμένες λεπτομέρειες.",
|
||||
"sameAsServerTimezone": "Ίδιο με τη ζώνη ώρας του Server",
|
||||
"startDateTime": "Ημερομηνία/Ώρα έναρξης",
|
||||
"endDateTime": "Ημερομηνία/Ώρα λήξης",
|
||||
"cronExpression": "Έκφραση Cron",
|
||||
"cronSchedule": "Πρόγραμμα: ",
|
||||
"enableNSCD": "Ενεργοποιήστε NSCD (Name Service Cache Daemon) για προσωρινή αποθήκευση όλων των αιτημάτων DNS",
|
||||
"chromeExecutable": "Εκτελέσιμο Chrome/Chromium",
|
||||
"chromeExecutableAutoDetect": "Αυτόματος Εντοπισμός",
|
||||
"Edit Maintenance": "Επεξεργασία Συντήρησης",
|
||||
"tagAlreadyStaged": "Η ετικέτα (όνομα και τιμή) είναι ήδη σε κατάσταση staged για αυτό το batch.",
|
||||
"Reset Token": "Επαναφορά Token",
|
||||
"defaultFriendlyName": "Νέα Οθόνη",
|
||||
"styleElapsedTimeShowNoLine": "Εμφάνισε (Χωρίς Γραμμή)",
|
||||
"telegramSendSilentlyDescription": "Στέλνει το μήνυμα αθόρυβα. Οι χρήστες θα λάβουν ειδοποίηση χωρίς ήχο.",
|
||||
"templateServiceName": "όνομα υπηρεσίας",
|
||||
"templateHostnameOrURL": "hostname ή URL",
|
||||
"templateStatus": "κατάσταση",
|
||||
"webhookBodyPresetOption": "Πρότυπο - {0}",
|
||||
"webhookBodyCustomOption": "Προσαρμοσμένο Σώμα",
|
||||
"and": "και",
|
||||
"Search monitored sites": "Αναζητήστε παρακολουθούμενα sites",
|
||||
"shrinkDatabaseDescriptionSqlite": "Πυροδοτήστε τη βαση δεδομένων {vacuum} για SQLite. Το {auto_vacuum} είναι ήδη ενεργοποιημένο αλλα αυτό δεν ανασυγκροτεί τη βάση ούτε επανενώνει μεμονωμένες σελίδες στη βάση όπως το κάνει η εντολή {vacuum}.",
|
||||
"Host URL": "Host URL",
|
||||
"locally configured mail transfer agent": "τοπικά παραμετροποιημένος πράκτορας μεταφοράς αλληλογραφίας",
|
||||
"Either enter the hostname of the server you want to connect to or localhost if you intend to use a locally configured mail transfer agent": "Εισάγετε είτε το hostname του server που θέλετε να συνδεθείτε ή {localhost} αν σκοπεύετε να χρησιμοποιήσετε την επιλογή {local_mta}",
|
||||
"now": "τώρα",
|
||||
"ignoredTLSError": "Έχουν αγνοηθεί τα σφάλματα TLS/SSL",
|
||||
"ignoreTLSErrorGeneral": "Παράβλεψη σφάλματος TLS/SSL για σύνδεση",
|
||||
"programmingLanguages": "Γλώσσες Προγραμματισμού",
|
||||
"styleElapsedTime": "Παρελθόν χρόνος υπό την μπάρα heartbeat",
|
||||
"pushOthers": "Άλλα",
|
||||
"time ago": "πριν από {0}",
|
||||
"-year": "-έτος",
|
||||
"Json Query Expression": "Έκφραση Json Query",
|
||||
"Request Timeout": "Timeout Αιτήματος",
|
||||
"timeoutAfter": "Λήξη χρόνου ύστερα από {0} δευτερόλεπτα",
|
||||
"pushViewCode": "Πώς να χρησιμοποιήσω Οθόνη Push (βλέπε κώδικα)"
|
||||
}
|
||||
|
@@ -71,6 +71,7 @@
|
||||
"locally configured mail transfer agent": "locally configured mail transfer agent",
|
||||
"Either enter the hostname of the server you want to connect to or localhost if you intend to use a locally configured mail transfer agent": "Either enter the hostname of the server you want to connect to or {localhost} if you intend to use a {local_mta}",
|
||||
"Port": "Port",
|
||||
"Path": "Path",
|
||||
"Heartbeat Interval": "Heartbeat Interval",
|
||||
"Request Timeout": "Request Timeout",
|
||||
"timeoutAfter": "Timeout after {0} seconds",
|
||||
@@ -266,6 +267,10 @@
|
||||
"Current User": "Current User",
|
||||
"topic": "Topic",
|
||||
"topicExplanation": "MQTT topic to monitor",
|
||||
"mqttWebSocketPath": "MQTT WebSocket Path",
|
||||
"mqttWebsocketPathExplanation": "WebSocket path for MQTT over WebSocket connections (e.g., /mqtt)",
|
||||
"mqttWebsocketPathInvalid": "Please use a valid WebSocket Path format",
|
||||
"mqttHostnameTip": "Please use this format {hostnameFormat}",
|
||||
"successKeyword": "Success Keyword",
|
||||
"successKeywordExplanation": "MQTT Keyword that will be considered as success",
|
||||
"recent": "Recent",
|
||||
@@ -587,6 +592,11 @@
|
||||
"rrtypeDescription": "Select the RR type you want to monitor",
|
||||
"pauseMonitorMsg": "Are you sure want to pause?",
|
||||
"enableDefaultNotificationDescription": "This notification will be enabled by default for new monitors. You can still disable the notification separately for each monitor.",
|
||||
"Clear All Events": "Clear All Events",
|
||||
"clearAllEventsMsg": "Are you sure want to delete all events?",
|
||||
"Events cleared successfully": "Events cleared successfully.",
|
||||
"No monitors found": "No monitors found.",
|
||||
"Could not clear events": "Could not clear {failed}/{total} events",
|
||||
"clearEventsMsg": "Are you sure want to delete all events for this monitor?",
|
||||
"clearHeartbeatsMsg": "Are you sure want to delete all heartbeats for this monitor?",
|
||||
"confirmClearStatisticsMsg": "Are you sure you want to delete ALL statistics?",
|
||||
@@ -640,6 +650,8 @@
|
||||
"pushyToken": "Device token",
|
||||
"apprise": "Apprise (Support 50+ Notification services)",
|
||||
"GoogleChat": "Google Chat (Google Workspace only)",
|
||||
"Template plain text instead of using cards": "Template plain text instead of using cards",
|
||||
"issueWithGoogleChatOnAndroidHelptext": "This also allows to get around bugs upstream like {issuetackerURL}",
|
||||
"wayToGetKookBotToken": "Create application and get your bot token at {0}",
|
||||
"wayToGetKookGuildID": "Switch on 'Developer Mode' in Kook setting, and right click the guild to get its ID",
|
||||
"Guild ID": "Guild ID",
|
||||
|
@@ -1106,7 +1106,64 @@
|
||||
"smseagleDuration": "Duración (en segundos)",
|
||||
"smseagleApiv1": "APIv1 (para proyectos existentes y retrocompatibilidad)",
|
||||
"smseagleApiv2": "APIv2 (recomendada para nuevas integraciones)",
|
||||
"templateServiceName": "Nombre del servicio",
|
||||
"templateHostnameOrURL": "Nombre del host o URL",
|
||||
"templateStatus": "Estado"
|
||||
"templateServiceName": "nombre de servicio",
|
||||
"templateHostnameOrURL": "Nombre de host o URL",
|
||||
"templateStatus": "estado",
|
||||
"mqttWebsocketPathInvalid": "Por favor, utiliza un formato válido de ruta WebSocket",
|
||||
"smseagleDocs": "Consulte la documentación o la disponibilidad de APIv2: {0}",
|
||||
"SpugPush Template Code": "Plantilla de código",
|
||||
"FlashDuty Push URL Placeholder": "Copiar de la página de la página de integración de alerta",
|
||||
"pingNumericLabel": "Salida numérica",
|
||||
"pingGlobalTimeoutDescription": "Tiempo total en segundos antes de que termine el ping, independientemente de los paquetes enviados",
|
||||
"customUrlDescription": "Se utilizará como URL en la que se puede hacer clic en lugar de la del monitor.",
|
||||
"pingNumericDescription": "Si se marca, se mostrarán las direcciones IP en vez de los nombres de host simbólicos",
|
||||
"pingGlobalTimeoutLabel": "Tiempo de espera global",
|
||||
"pingPerRequestTimeoutLabel": "Tiempo de espera por ping",
|
||||
"Custom URL": "URL personalizada",
|
||||
"OneChatUserIdOrGroupId": "ID de usuario o ID de grupo de OneChat",
|
||||
"OneChatAccessToken": "Token de acceso de OneChat",
|
||||
"wayToGetWahaApiUrl": "La URL de tu instancia WAHA.",
|
||||
"wahaSession": "Sesión",
|
||||
"pingPerRequestTimeoutDescription": "Este es el tiempo de espera máximo (en segundos) antes de considerar que se ha perdido un paquete",
|
||||
"OneChatBotId": "ID de bot de OneChat",
|
||||
"mqttHostnameTip": "Por favor, utiliza este formato {hostnameFormat}",
|
||||
"smseagleComma": "Multiple debe separarse con comas",
|
||||
"pingCountLabel": "Paquetes máximos",
|
||||
"pingCountDescription": "Número de paquetes a enviar antes de parar",
|
||||
"ntfyPriorityDown": "Prioridad para eventos de caída",
|
||||
"pause": "Pausar",
|
||||
"wayToGetWahaSession": "Desde esta sesión, WAHA envía notificaciones a Chat ID. Puedes encontrarlo en el Panel de WAHA.",
|
||||
"wayToWriteWahaChatId": "El número de teléfono con el prefijo internacional, pero sin el signo más al principio ({0}), el ID de contacto ({1}) o el ID de grupo ({2}). Las notificaciones se envían a este ID de chat desde la sesión de WAHA.",
|
||||
"YZJ Webhook URL": "URL del webhook de YZJ",
|
||||
"Add Another Tag": "Añadir otra etiqueta",
|
||||
"Manual": "Manual",
|
||||
"wahaChatId": "ID del chat (Número de teléfono / ID de contacto / ID de grupo)",
|
||||
"smtpHelpText": "\"SMTPS\" comprueba el funcionamiento de SMTP/TLS; \"Ignorar TLS\" se conecta mediante texto plano; \"STARTTLS\" se conecta, emite un comando STARTTLS y verifica el certificado del servidor. Ninguno de estos envía un correo electrónico.",
|
||||
"Plain Text": "Texto plano",
|
||||
"Message Template": "Plantilla de mensaje",
|
||||
"Template Format": "Formato de plantilla",
|
||||
"wayToGetWahaApiKey": "La clave API es el valor de la variable de entorno WHATSAPP_API_KEY que utilizabas para ejecutar WAHA.",
|
||||
"smsplanetApiDocs": "Puede encontrar información detallada sobre la obtención de tokens API en {the_smsplanet_documentation}.",
|
||||
"the smsplanet documentation": "Documentación de smsplanet",
|
||||
"Phone numbers": "Números de teléfono",
|
||||
"Sender name": "Nombre del emisor",
|
||||
"smsplanetNeedToApproveName": "Necesita ser aprobado en el panel de cliente",
|
||||
"smsplanetApiToken": "Token para la API de SMSPlanet",
|
||||
"Disable URL in Notification": "Deshabilitar la URL en la notificación",
|
||||
"YZJ Robot Token": "Token del robot de YZJ",
|
||||
"pingIntervalAdjustedInfo": "Intervalo ajustado en función del recuento de paquetes, el tiempo de espera global y el tiempo de espera por ping",
|
||||
"Clear Form": "Limpiar formulario",
|
||||
"Ip Family": "Familia de IP",
|
||||
"ipFamilyDescriptionAutoSelect": "Utiliza {happyEyeballs} para determinar la familia de IP.",
|
||||
"Happy Eyeballs algorithm": "Algoritmo Happy Eyeballs",
|
||||
"Path": "Ruta",
|
||||
"mqttWebSocketPath": "Ruta de MQTT WebSocket",
|
||||
"smseagleMsgRing": "Llamada",
|
||||
"Clear All Events": "Limpiar todos los eventos",
|
||||
"clearAllEventsMsg": "Seguro deseas eliminar todos los eventos?",
|
||||
"Events cleared successfully": "Eventos limpiados exitosamente.",
|
||||
"No monitors found": "No se encontraron monitores.",
|
||||
"Could not clear events": "No se pudieron limpiar {failed}/{total} eventos",
|
||||
"smseagleGroupV2": "ID(s) de grupo(s) de Guía Telefónica",
|
||||
"mqttWebsocketPathExplanation": "Ruta del WebSocker para MQTT sobre conexion WebSocker (ejemplo, /mqtt)"
|
||||
}
|
||||
|
@@ -1175,5 +1175,17 @@
|
||||
"Ip Family": "Famille d'adresses IP",
|
||||
"ipFamilyDescriptionAutoSelect": "Utilise le {happyEyeballs} pour déterminer la famille d'adresses IP.",
|
||||
"OAuth Audience": "Audience OAuth",
|
||||
"Optional: The audience to request the JWT for": "Optionnel : Le public pour lequel demander le JWT"
|
||||
"Optional: The audience to request the JWT for": "Optionnel : Le public pour lequel demander le JWT",
|
||||
"Path": "Chemin",
|
||||
"mqttWebSocketPath": "Chemin WebSocket MQTT",
|
||||
"mqttWebsocketPathExplanation": "Chemin WebSocket pour MQTT sur les connexions WebSocket (par exemple, /mqtt)",
|
||||
"mqttWebsocketPathInvalid": "Veuillez utiliser un format de chemin WebSocket valide",
|
||||
"mqttHostnameTip": "Veuillez utiliser ce format {hostnameFormat}",
|
||||
"Template plain text instead of using cards": "Modèle en texte brut au lieu d'utiliser des cartes",
|
||||
"issueWithGoogleChatOnAndroidHelptext": "Cela permet également de contourner les bugs en amont comme {issuetackerURL}",
|
||||
"clearAllEventsMsg": "Êtes-vous sûr de vouloir supprimer tous les événements ?",
|
||||
"Clear All Events": "Effacer tous les événements",
|
||||
"Events cleared successfully": "Événements effacés avec succès.",
|
||||
"No monitors found": "Aucun sonde trouvé.",
|
||||
"Could not clear events": "Impossible d’effacer {failed}/{total} événements"
|
||||
}
|
||||
|
@@ -728,7 +728,7 @@
|
||||
"smseagleRecipient": "Faighteoir(í) (ní mór an iliomad a bheith scartha le camóg)",
|
||||
"smseagleToken": "Comhartha rochtana API",
|
||||
"smseagleUrl": "URL do ghléis SMSEagle",
|
||||
"smseaglePriority": "Tosaíocht na teachtaireachta (0-9, réamhshocraithe = 0)",
|
||||
"smseaglePriority": "Tosaíocht teachtaireachta (0-9, an tosaíocht is airde = 9)",
|
||||
"Recipient Number": "Uimhir Faighteoir",
|
||||
"From Name/Number": "Ó Ainm/Uimhir",
|
||||
"Octopush API Version": "Leagan API Octopush",
|
||||
@@ -859,7 +859,7 @@
|
||||
"Browser Screenshot": "Scáileán Brabhsálaí",
|
||||
"What is a Remote Browser?": "Cad is Brabhsálaí Cianda ann?",
|
||||
"serwersmsSenderName": "Ainm Seoltóra SMS (cláraithe trí thairseach custaiméirí)",
|
||||
"smseagleEncoding": "Seol mar Unicode",
|
||||
"smseagleEncoding": "Seol mar Unicode (réamhshocrú=GSM-7)",
|
||||
"Leave blank to use a shared sender number.": "Fág bán chun uimhir seoltóra roinnte a úsáid.",
|
||||
"onebotGroupMessage": "Grúpa",
|
||||
"onebotUserOrGroupId": "Aitheantas Grúpa/Úsáideora",
|
||||
@@ -879,7 +879,7 @@
|
||||
"monitorToastMessagesDescription": "Imíonn fógraí tósta le haghaidh monatóirí tar éis am tugtha i soicindí. Díchumasaítear am istigh le socrú go -1. Díchumasaigh Socrú go 0 fógraí tósta.",
|
||||
"Enable Kafka Producer Auto Topic Creation": "Cumasaigh Cruthú Uath-Ábhair Táirgeora Kafka",
|
||||
"noGroupMonitorMsg": "Níl sé ar fáil. Cruthaigh Monatóir Grúpa ar dtús.",
|
||||
"wayToGetFlashDutyKey": "Is féidir leat dul go Cainéal -> (Roghnaigh Cainéal) -> Comhtháthaithe -> Cuir leathanach comhtháthú nua leis, cuir 'Uptime Kuma' leis chun seoladh brúigh a fháil, cóipeáil an Eochair Chomhtháthaithe sa seoladh. Le haghaidh tuilleadh eolais, tabhair cuairt le do thoil",
|
||||
"wayToGetFlashDutyKey": "Chun Uptime Kuma a chomhtháthú le Flashduty: Téigh go Cainéil > Roghnaigh cainéal > Comhtháthúcháin > Cuir comhtháthú nua leis, roghnaigh Uptime Kuma, agus cóipeáil an URL Push.",
|
||||
"gamedigGuessPortDescription": "Féadfaidh an calafort a úsáideann Prótacal Iarratas Freastalaí Comhla a bheith difriúil ó phort an chliaint. Bain triail as seo mura bhfuil an monatóir in ann ceangal le do fhreastalaí.",
|
||||
"successBackupRestored": "Tá an cúltaca athchóirithe go rathúil.",
|
||||
"Host URL": "URL Óstach",
|
||||
@@ -1053,5 +1053,82 @@
|
||||
"Clear": "Glan",
|
||||
"Elevator": "Ardaitheoir",
|
||||
"Guitar": "Giotár",
|
||||
"Scifi": "Ficsean eolaíochta"
|
||||
"Scifi": "Ficsean eolaíochta",
|
||||
"ipFamilyDescriptionAutoSelect": "Úsáideann sé {happyEyeballs} chun an teaghlach IP a chinneadh.",
|
||||
"Happy Eyeballs algorithm": "Algartam Súile Sona",
|
||||
"Ip Family": "Teaghlach IP",
|
||||
"Manual": "Lámhleabhar",
|
||||
"OAuth Audience": "Lucht Féachana OAuth",
|
||||
"pingGlobalTimeoutLabel": "Am Teorann Domhanda",
|
||||
"pingPerRequestTimeoutLabel": "Am Teorann In aghaidh an Phing",
|
||||
"pingPerRequestTimeoutDescription": "Seo an t-am feithimh uasta (i soicindí) sula measfar go bhfuil paicéad ping amháin caillte",
|
||||
"pingIntervalAdjustedInfo": "Eatramh coigeartaithe bunaithe ar chomhaireamh paicéad, am scoir domhanda agus am scoir in aghaidh an phing",
|
||||
"Custom URL": "URL Saincheaptha",
|
||||
"customUrlDescription": "Úsáidfear é mar an URL inchliceáilte in ionad URL an mhonatóra.",
|
||||
"OneChatBotId": "Aitheantas Bot OneChat",
|
||||
"Plain Text": "Téacs Gnáth",
|
||||
"Disable URL in Notification": "Díchumasaigh URL san Fhógra",
|
||||
"Add Another Tag": "Cuir Clib Eile leis",
|
||||
"Staged Tags for Batch Add": "Clibeanna Céimnithe le haghaidh Cuir Baisc leis",
|
||||
"Clear Form": "Foirm Glan",
|
||||
"pause": "Sos",
|
||||
"pingCountDescription": "Líon na bpacáistí le seoladh sula stopann tú",
|
||||
"smsplanetApiDocs": "Is féidir faisnéis mhionsonraithe maidir le comharthaí API a fháil a fháil i {the_smsplanet_documentation}.",
|
||||
"the smsplanet documentation": "an doiciméadú smsplanet",
|
||||
"Phone numbers": "Uimhreacha gutháin",
|
||||
"Sender name": "Ainm an tseoltóra",
|
||||
"smsplanetApiToken": "Comhartha don SMSPlanet API",
|
||||
"defaultFriendlyName": "Monatóir Nua",
|
||||
"telegramUseTemplate": "Úsáid teimpléad teachtaireachta saincheaptha",
|
||||
"telegramUseTemplateDescription": "Má tá sé cumasaithe, seolfar an teachtaireacht ag baint úsáide as teimpléad saincheaptha.",
|
||||
"telegramServerUrlDescription": "Chun teorainneacha bot api Telegram a ardú nó rochtain a fháil i gceantair atá blocáilte (an tSín, an Iaráin, srl.). Le haghaidh tuilleadh eolais cliceáil {0}. Réamhshocrú: {1}",
|
||||
"Use HTML for custom E-mail body": "Úsáid HTML le haghaidh corp saincheaptha ríomhphoist",
|
||||
"smseagleGroupV2": "Aitheantóirí grúpa leabhar teileafóin",
|
||||
"smseagleMsgType": "Cineál teachtaireachta",
|
||||
"smseagleMsgTts": "Glao téacs-go-hurlabhra",
|
||||
"smseagleApiv1": "APIv1 (do thionscadail atá ann cheana féin agus comhoiriúnacht siar)",
|
||||
"Optional: The audience to request the JWT for": "Roghnach: An lucht féachana ar a n-iarrfar an JWT",
|
||||
"pingCountLabel": "Uasmhéid Pacáistí",
|
||||
"pingNumericDescription": "Más seiceáilte é, aschuirfear seoltaí IP in ionad ainmneacha óstach siombalacha",
|
||||
"pingGlobalTimeoutDescription": "Am iomlán i soicindí sula stopann an ping, beag beann ar na paicéid a seoladh",
|
||||
"smtpHelpText": "Déanann 'SMTPS' tástáil an bhfuil SMTP/TLS ag obair; ceanglaíonn 'Neamhaird a dhéanamh de TLS' thar théacs simplí; ceanglaíonn 'STARTTLS', eisíonn sé ordú STARTTLS agus fíoraíonn sé teastas an fhreastalaí. Ní sheolann aon cheann díobh seo ríomhphost.",
|
||||
"OneChatAccessToken": "Comhartha Rochtana OneChat",
|
||||
"OneChatUserIdOrGroupId": "Aitheantas Úsáideora OneChat nó Aitheantas Grúpa",
|
||||
"wayToGetWahaSession": "Is é Eochair API luach athróg comhshaoil WHATSAPP_API_KEY a d’úsáid tú chun WAHA a rith.",
|
||||
"wayToWriteWahaChatId": "An uimhir theileafóin leis an réimír idirnáisiúnta, ach gan an comhartha móide ag an tús ({0}), an ID Teagmhála ({1}) ná an ID Grúpa ({2}). Seoltar fógraí chuig an ID Comhrá seo ó Sheisiún WAHA.",
|
||||
"Font Twemoji by Twitter licensed under": "Cló Twemoji le Twitter ceadúnaithe faoi",
|
||||
"smsplanetNeedToApproveName": "Ní mór é a cheadú sa phainéal cliant",
|
||||
"wahaSession": "Seisiún",
|
||||
"wahaChatId": "ID Comhrá (Uimhir Theileafóin / ID Teagmhála / ID Grúpa)",
|
||||
"wayToGetWahaApiUrl": "URL d’Áisnéis WAHA.",
|
||||
"wayToGetWahaApiKey": "Is é Eochair API luach athróg comhshaoil WHATSAPP_API_KEY a d'úsáid tú chun WAHA a rith.",
|
||||
"ntfyPriorityHelptextPriorityHigherThanDown": "Ba chóir go mbeadh an tosaíocht rialta níos airde ná tosaíocht {0}. Tá tosaíocht {1} níos airde ná tosaíocht {0} {2}",
|
||||
"ntfyPriorityDown": "Tosaíocht do imeachtaí SÍOS",
|
||||
"Message Template": "Teimpléad Teachtaireachta",
|
||||
"Template Format": "Formáid Teimpléid",
|
||||
"YZJ Robot Token": "Comhartha robot YZJ",
|
||||
"YZJ Webhook URL": "YZJ Webhook URL",
|
||||
"Add Tags": "Cuir Clibeanna leis",
|
||||
"tagAlreadyOnMonitor": "Tá an clib seo (ainm agus luach) ar an monatóir cheana féin nó á cur leis ar feitheamh.",
|
||||
"tagAlreadyStaged": "Tá an clib seo (ainm agus luach) curtha i láthair cheana féin don bhaisc seo.",
|
||||
"tagNameExists": "Tá clib chórais leis an ainm seo ann cheana féin. Roghnaigh é ón liosta nó bain úsáid as ainm eile.",
|
||||
"templateStatus": "stádas",
|
||||
"telegramTemplateFormatDescription": "Ceadaíonn Telegram teangacha marcála éagsúla a úsáid le haghaidh teachtaireachtaí, féach Telegram {0} le haghaidh sonraí sonracha.",
|
||||
"telegramServerUrl": "(Roghnach) URL an Fhreastalaí",
|
||||
"smseagleContactV2": "Aitheantóirí teagmhála an leabhair teileafóin",
|
||||
"smseagleMsgSms": "Teachtaireacht SMS (réamhshocraithe)",
|
||||
"smseagleMsgRing": "Glaoigh glaoch",
|
||||
"smseagleMsgTtsAdvanced": "Glao ardleibhéil téacs-go-hurlabhra",
|
||||
"smseagleDuration": "Fad (i soicindí)",
|
||||
"smseagleTtsModel": "Aitheantas samhail téacs-go-hurlabhra",
|
||||
"smseagleApiType": "Leagan API",
|
||||
"smseagleApiv2": "APIv2 (molta le haghaidh comhtháthúcháin nua)",
|
||||
"smseagleDocs": "Seiceáil an doiciméadacht nó infhaighteacht APIv2: {0}",
|
||||
"smseagleComma": "Ní mór ilchodanna a dheighilt le camóg",
|
||||
"SpugPush Template Code": "Cód Teimpléid",
|
||||
"FlashDuty Push URL": "Brúigh URL",
|
||||
"FlashDuty Push URL Placeholder": "Cóipeáil ón leathanach comhtháthaithe foláirimh",
|
||||
"pingNumericLabel": "Aschur Uimhriúil",
|
||||
"templateServiceName": "ainm na seirbhíse",
|
||||
"templateHostnameOrURL": "ainm óstach nó URL"
|
||||
}
|
||||
|
@@ -819,7 +819,7 @@
|
||||
"statusMaintenance": "Održavanje",
|
||||
"General Monitor Type": "Općeniti tip Monitora",
|
||||
"Maintenance": "Održavanje",
|
||||
"Specific Monitor Type": "Određeni tip Monitora",
|
||||
"Specific Monitor Type": "Posebni tip Monitora",
|
||||
"Monitor": "Monitor | Monitori",
|
||||
"Invert Keyword": "Obrni ključnu riječ",
|
||||
"filterActive": "Aktivnost",
|
||||
@@ -1169,5 +1169,12 @@
|
||||
"Disable URL in Notification": "Onemogući URL u obavijesti",
|
||||
"Manual": "Ručno",
|
||||
"OAuth Audience": "OAuth publika",
|
||||
"Optional: The audience to request the JWT for": "Neobavezno: Publika za koju se traži JWT"
|
||||
"Optional: The audience to request the JWT for": "Neobavezno: Publika za koju se traži JWT",
|
||||
"mqttWebSocketPath": "Putanja MQTT WebSocketa",
|
||||
"mqttHostnameTip": "Koristiti ovaj format {hostnameFormat}",
|
||||
"Path": "Putanja",
|
||||
"mqttWebsocketPathExplanation": "Putanja WebSocketa za MQTT preko WebSocket veza (npr. /mqtt)",
|
||||
"mqttWebsocketPathInvalid": "Koristiti valjani format putanje WebSocketa",
|
||||
"Template plain text instead of using cards": "Predložak običnim tekstom umjesto korištenja kartica",
|
||||
"issueWithGoogleChatOnAndroidHelptext": "Ovo omogućuje izbjegavanje grešaka poput {issuetackerURL}"
|
||||
}
|
||||
|
@@ -41,7 +41,7 @@
|
||||
"Check Update On GitHub": "Cek Pembaruan di GitHub",
|
||||
"List": "Daftar",
|
||||
"Add": "Tambah",
|
||||
"Add New Monitor": "Tambah Monitor Baru",
|
||||
"Add New Monitor": "Tambahkan Monitor Baru",
|
||||
"Quick Stats": "Statistik Singkat",
|
||||
"Up": "Aktif",
|
||||
"Down": "Tidak Aktif",
|
||||
@@ -53,7 +53,7 @@
|
||||
"DateTime": "Tanggal/Waktu",
|
||||
"Message": "Pesan",
|
||||
"No important events": "Tidak ada peristiwa penting",
|
||||
"Resume": "Lanjut",
|
||||
"Resume": "Melanjutkan",
|
||||
"Edit": "Ubah",
|
||||
"Delete": "Hapus",
|
||||
"Current": "Saat ini",
|
||||
@@ -71,9 +71,9 @@
|
||||
"URL": "URL",
|
||||
"Hostname": "Nama Host",
|
||||
"Port": "Port",
|
||||
"Heartbeat Interval": "Interval Heartbeat",
|
||||
"Heartbeat Interval": "Waktu Jeda Heartbeat",
|
||||
"Retries": "Mencoba lagi",
|
||||
"Heartbeat Retry Interval": "Interval Pengulangan Heartbeat",
|
||||
"Heartbeat Retry Interval": "Waktu Jeda Pengulangan Heartbeat",
|
||||
"Resend Notification if Down X times consecutively": "Kirim Ulang Pemberitahuan jika Tidak Aktif X kali berturut-turut",
|
||||
"Advanced": "Tingkat Lanjut",
|
||||
"Upside Down Mode": "Mode Terbalik",
|
||||
@@ -102,8 +102,8 @@
|
||||
"New Password": "Kata Sandi Baru",
|
||||
"Repeat New Password": "Ulangi Kata Sandi Baru",
|
||||
"Update Password": "Perbarui Kata Sandi",
|
||||
"Disable Auth": "Nonaktifkan Auth",
|
||||
"Enable Auth": "Aktifkan Auth",
|
||||
"Disable Auth": "Nonaktifkan autentikasi",
|
||||
"Enable Auth": "Aktifkan autentikasi",
|
||||
"disableauth.message1": "Apakah Anda yakin ingin {disableAuth}?",
|
||||
"disable authentication": "menonaktifkan autentikasi",
|
||||
"disableauth.message2": "Ini dirancang untuk skenario {intendThirdPartyAuth} di depan Uptime Kuma seperti Cloudflare Access, Authelia, atau mekanisme autentikasi lainnya.",
|
||||
@@ -126,7 +126,7 @@
|
||||
"Test": "Tes",
|
||||
"Certificate Info": "Info Sertifikat",
|
||||
"Resolver Server": "Server Penyelesai",
|
||||
"Resource Record Type": "Jenis Rekaman Sumber Daya",
|
||||
"Resource Record Type": "Jenis Rekaman Sumber",
|
||||
"Last Result": "Hasil Terakhir",
|
||||
"Create your admin account": "Buat akun admin Anda",
|
||||
"Repeat Password": "Ulangi Kata Sandi",
|
||||
@@ -154,7 +154,7 @@
|
||||
"Options": "Opsi",
|
||||
"Keep both": "Simpan keduanya",
|
||||
"Verify Token": "Verifikasi Token",
|
||||
"Setup 2FA": "Siapkan 2FA",
|
||||
"Setup 2FA": "Atur 2FA",
|
||||
"Enable 2FA": "Aktifkan 2FA",
|
||||
"Disable 2FA": "Nonaktifkan 2FA",
|
||||
"2FA Settings": "Pengaturan 2FA",
|
||||
@@ -186,8 +186,8 @@
|
||||
"All Systems Operational": "Semua Sistem Berfungsi",
|
||||
"Partially Degraded Service": "Layanan Terdegradasi Sebagian",
|
||||
"Degraded Service": "Layanan Terdegradasi",
|
||||
"Add Group": "Tambah Grup",
|
||||
"Add a monitor": "Tambah monitor",
|
||||
"Add Group": "Tambahkan Grup",
|
||||
"Add a monitor": "Tambahkan monitor",
|
||||
"Edit Status Page": "Edit Halaman Status",
|
||||
"Go to Dashboard": "Pergi ke Dasbor",
|
||||
"Status Page": "Halaman Status",
|
||||
@@ -877,7 +877,7 @@
|
||||
"templateMsg": "pesan pemberitahuan",
|
||||
"templateHeartbeatJSON": "objek yang menggambarkan heartbeat",
|
||||
"templateMonitorJSON": "objek yang menggambarkan monitor",
|
||||
"templateLimitedToUpDownCertNotifications": "hanya tersedia untuk notifikasi AKTIF/TIDAK AKTIF/Sertifikat yang kedaluwarsa",
|
||||
"templateLimitedToUpDownCertNotifications": "hanya tersedia untuk notifikasi AKTIF/TIDAK AKTIF/Sertifikat yang kadaluwarsa",
|
||||
"templateLimitedToUpDownNotifications": "hanya tersedia untuk notifikasi AKTIF/TIDAK AKTIF",
|
||||
"liquidIntroduction": "Kemampuan templat dicapai melalui bahasa templating Liquid. Silakan lihat {0} untuk petunjuk penggunaan. Ini adalah variabel yang tersedia:",
|
||||
"selectedMonitorCount": "Terpilih: {0}",
|
||||
@@ -1088,5 +1088,45 @@
|
||||
"The phone number of the recipient in E.164 format.": "Nomor telepon penerima dalam format E.164.",
|
||||
"Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies.": "Antara ID pengirim teks atau nomor telepon dalam format E.164 jika Anda ingin menerima balasan.",
|
||||
"SendGrid API Key": "Kunci API SendGrid",
|
||||
"Separate multiple email addresses with commas": "Pisahkan beberapa alamat email dengan koma"
|
||||
"Separate multiple email addresses with commas": "Pisahkan beberapa alamat email dengan koma",
|
||||
"telegramUseTemplate": "Gunakan pesan kustom templat",
|
||||
"telegramTemplateFormatDescription": "Telegram mengijinkan menggunakan berbagai bahasa markup untuk pesan, lihat Telegram {0} untuk detail lebih lanjut",
|
||||
"telegramServerUrlDescription": "Untuk menghapus batasan API bot Telegram atau mendapatkan akses di area yang diblokir (Seperti: China, Iran, dll.). Untuk informasi selengkapnya, klik {0}. Default: {1}",
|
||||
"Use HTML for custom E-mail body": "Gunakan HTML untuk kustom E-mail body",
|
||||
"Template plain text instead of using cards": "Templat teks biasa daripada menggunakan kartu",
|
||||
"issueWithGoogleChatOnAndroidHelptext": "Hal ini juga memungkinkan untuk mengatasi bug di upstream seperti {issuetackerURL}",
|
||||
"smseagleMsgTtsAdvanced": "Panggilan lanjutan Text-to-speech",
|
||||
"smseagleDocs": "Cek dokumentasi atau ketersedian APIv2: {0}",
|
||||
"SpugPush Template Code": "Kode templat",
|
||||
"ntfyPriorityHelptextPriorityHigherThanDown": "Prioritas biasa harus lebih tinggi daripada prioritas {0}. Prioritas {1} lebih tinggi daripada {0} prioritas {2}",
|
||||
"defaultFriendlyName": "Monitor Baru",
|
||||
"telegramServerUrl": "(Opsional) Url Server",
|
||||
"Path": "Path",
|
||||
"mqttWebSocketPath": "MQTT WebSocket Path",
|
||||
"mqttWebsocketPathExplanation": "Path WebSocket untuk MQTT yang melalui koneksi WebSocket (e.g., /mqtt)",
|
||||
"mqttWebsocketPathInvalid": "Harap gunakan WebSocket Path format yang benar",
|
||||
"mqttHostnameTip": "Harap gunakan format ini {hostnameFormat}",
|
||||
"smseagleComma": "Beberapa harus dipisahkan dengan koma",
|
||||
"Add Tags": "Tambahkan Tanda",
|
||||
"tagAlreadyOnMonitor": "Tanda ini (name and value) sudah tersedia di monitor atau penambahan tertunda.",
|
||||
"tagAlreadyStaged": "Tanda ini (name and value) sudah disiapkan untuk batch ini",
|
||||
"tagNameExists": "Tanda system dengan nama ini telah tersedia. Silakan pilih dari daftar atau gunakan nama lainnya",
|
||||
"smseagleGroupV2": "ID grup Phonebook",
|
||||
"smseagleContactV2": "ID kontak Phonebook",
|
||||
"smseagleMsgType": "Tipe Pesan",
|
||||
"smseagleMsgSms": "Pesan SMS (default)",
|
||||
"smseagleMsgRing": "Panggilan dering",
|
||||
"smseagleMsgTts": "Panggilan teks-ke-ucapan",
|
||||
"smseagleDuration": "Durasi (dalam detik)",
|
||||
"smseagleTtsModel": "ID model tulisan-ke-ucapan",
|
||||
"smseagleApiType": "Versi API",
|
||||
"smseagleApiv1": "APIv1 (Untuk projek yang telah ada dan kompabilitas ke belakang)",
|
||||
"smseagleApiv2": "APIv2 (rekomendasi untuk integrasi baru)",
|
||||
"ntfyPriorityDown": "Prioritas untuk kejadian DOWN",
|
||||
"FlashDuty Push URL": "Push URL",
|
||||
"FlashDuty Push URL Placeholder": "Salin dari halaman integrasi peringatan",
|
||||
"templateServiceName": "nama service",
|
||||
"templateHostnameOrURL": "Nama Host atau URL",
|
||||
"templateStatus": "Status",
|
||||
"telegramUseTemplateDescription": "Jika diaktifkan, pesan akan terkirim menggunakan templat kustom"
|
||||
}
|
||||
|
@@ -105,7 +105,7 @@
|
||||
"disableauth.message2": "{intendThirdPartyAuth} prima di Uptime Kuma, come ad esempio Cloudflare Access.",
|
||||
"where you intend to implement third-party authentication": "Questa opzione è per chi possiede un sistema di autenticazione gestito da terze parti",
|
||||
"Please use this option carefully!": "Utilizzare con attenzione!",
|
||||
"Logout": "Esci",
|
||||
"Logout": "Disconnettiti",
|
||||
"Leave": "Annulla",
|
||||
"I understand, please disable": "Lo capisco, disabilitare l'autenticazione",
|
||||
"Confirm": "Conferma",
|
||||
@@ -153,7 +153,7 @@
|
||||
"Setup 2FA": "Configura 2FA",
|
||||
"Enable 2FA": "Abilita 2FA",
|
||||
"Disable 2FA": "Disabilita 2FA",
|
||||
"2FA Settings": "Gestisci l'autenticazione a due fattori",
|
||||
"2FA Settings": "Gestisci 2FA",
|
||||
"Two Factor Authentication": "Autenticazione a due fattori (2FA)",
|
||||
"Active": "Attivata",
|
||||
"Inactive": "Disattivata",
|
||||
@@ -779,5 +779,6 @@
|
||||
"Guild ID": "Guild ID",
|
||||
"Free Mobile API Key": "Chiave API mobile gratuita",
|
||||
"telegramUseTemplate": "Utilizza un template di messaggio personalizzato",
|
||||
"telegramTemplateFormatDescription": "Telegram permette l'utilizzo di diversi linguaggi di markup, vedi Telegram {0} per maggiori dettagli."
|
||||
"telegramTemplateFormatDescription": "Telegram permette l'utilizzo di diversi linguaggi di markup, vedi Telegram {0} per maggiori dettagli.",
|
||||
"Add Tags": "Aggiungi Etichette"
|
||||
}
|
||||
|
@@ -749,7 +749,7 @@
|
||||
"monitorToastMessagesDescription": "モニターのトースト通知は、指定された秒数後に消えます。-1に設定するとタイムアウトが無効になり、0に設定するとトースト通知が無効になります。",
|
||||
"Pick a SASL Mechanism...": "SASLメカニズムを選択してください",
|
||||
"noGroupMonitorMsg": "利用できません。先にグループモニターを作成してください。",
|
||||
"wayToGetFlashDutyKey": "チャンネル -> (チャンネルを選択) -> 統合 -> 新しい統合を追加 のページに移動し、「Uptime Kuma」を追加してプッシュアドレスを取得し、アドレス内の統合キーをコピーしてください。詳細はこちら:",
|
||||
"wayToGetFlashDutyKey": "UptimeKumaとFlashdutyを統合するには: Channels > Select a channel > Integrations > Add a new integrationでUptime Kumaを選択し、Push URLをコピーしてください。",
|
||||
"cacheBusterParamDescription": "キャッシュをスキップするためにランダム生成したパラメータ",
|
||||
"gamedigGuessPortDescription": "Valve Server Query Protocolで使用されるポートはクライアントポートとは異なる場合があります。モニターがサーバーに接続できない場合は、この設定を試してください。",
|
||||
"receiverInfoSevenIO": "受信側番号がドイツの番号ではない場合、番号の前に国コードを追加する必要があります(例:アメリカの国コード1の場合は、017612121212の代わりに117612121212を使用します)。",
|
||||
@@ -868,8 +868,8 @@
|
||||
"smseagleRecipientType": "受信者タイプ",
|
||||
"smseagleToken": "APIアクセストークン",
|
||||
"smseagleUrl": "SMSEagleデバイスURL",
|
||||
"smseagleEncoding": "Unicodeで送信",
|
||||
"smseaglePriority": "メッセージ優先度 (0-9, default = 0)",
|
||||
"smseagleEncoding": "Unicodeで送信 (default=GSM-7)",
|
||||
"smseaglePriority": "メッセージ優先度 (0-9, 最大優先度 = 9)",
|
||||
"smspartnerApiurl": "APIキーはダッシュボードから確認できます: {0}",
|
||||
"smspartnerPhoneNumber": "電話番号",
|
||||
"smspartnerSenderName": "SMS送信者名",
|
||||
@@ -1091,5 +1091,56 @@
|
||||
"Sender name": "送信者名",
|
||||
"smsplanetNeedToApproveName": "クライアントパネルでの承認が必要",
|
||||
"smsplanetApiToken": "SMSPlanet APIのトークン",
|
||||
"smsplanetApiDocs": "APIトークンの取得に関する詳細な情報は、{the_smsplanet_documentation}にあります。"
|
||||
"smsplanetApiDocs": "APIトークンの取得に関する詳細な情報は、{the_smsplanet_documentation}にあります。",
|
||||
"Happy Eyeballs algorithm": "Happy Eyeballs アルゴリズム",
|
||||
"Ip Family": "IPファミリー",
|
||||
"ipFamilyDescriptionAutoSelect": "IPファミリーの決定に {happyEyeballs} を使用する。",
|
||||
"Manual": "マニュアル",
|
||||
"pingNumericLabel": "数値出力",
|
||||
"pingGlobalTimeoutLabel": "グローバルタイムアウト",
|
||||
"pingGlobalTimeoutDescription": "送信されたパケットに関係なく、pingが停止するまでの合計時間(秒)",
|
||||
"pingPerRequestTimeoutLabel": "Pingごとのタイムアウト",
|
||||
"pingIntervalAdjustedInfo": "パケット数、グローバルタイムアウト、Pingごとのタイムアウトに基づいて間隔を調整",
|
||||
"OneChatUserIdOrGroupId": "OneChat ユーザーIDまたはグループID",
|
||||
"OneChatBotId": "OneChat ボットID",
|
||||
"Add Another Tag": "その他のタグを追加",
|
||||
"Staged Tags for Batch Add": "一括追加用ステージタグ",
|
||||
"Clear Form": "フォームをクリア",
|
||||
"pause": "一時停止",
|
||||
"tagNameExists": "この名前のシステムタグは既に存在します。リストから選択するか、別の名前を使用してください。",
|
||||
"smseagleDocs": "ドキュメントまたはAPIv2の可用性をチェックする: {0}",
|
||||
"OAuth Audience": "OAuth オーディエンス",
|
||||
"Optional: The audience to request the JWT for": "オプション: JWTを要求するオーディエンス",
|
||||
"pingCountDescription": "停止前に送信するパケット数",
|
||||
"pingNumericDescription": "チェックした場合、シンボリックホスト名の代わりにIPアドレスが出力されます",
|
||||
"pingPerRequestTimeoutDescription": "これは、1つのpingパケットが失われたとみなすまでの最大待機時間(秒)です",
|
||||
"Disable URL in Notification": "通知のURLを無効にする",
|
||||
"defaultFriendlyName": "新しいモニター",
|
||||
"smseagleGroupV2": "電話帳グループID",
|
||||
"smseagleContactV2": "電話帳連絡ID",
|
||||
"ntfyPriorityHelptextPriorityHigherThanDown": "通常の優先度は {0} 優先度より高い必要があります。優先度 {1} は優先度 {0} 優先度 {2} よりも高い",
|
||||
"ntfyPriorityDown": "ダウンイベントの優先順位",
|
||||
"pingCountLabel": "最大パケット",
|
||||
"Add Tags": "タグを追加",
|
||||
"tagAlreadyOnMonitor": "このタグ(名前と値)はモニター上に既にあるか、追加待ちです。",
|
||||
"tagAlreadyStaged": "このタグ(名前と値)は、このバッチに対して既にステージングされています。",
|
||||
"Use HTML for custom E-mail body": "カスタムメール本文にHTMLを使用する",
|
||||
"smseagleMsgType": "メッセージタイプ",
|
||||
"smseagleMsgSms": "SMS メッセージ (デフォルト)",
|
||||
"smseagleMsgRing": "呼び出し音",
|
||||
"smseagleMsgTts": "音声合成通話",
|
||||
"smseagleMsgTtsAdvanced": "音声合成高度通話",
|
||||
"smseagleDuration": "継続時間(秒)",
|
||||
"smseagleTtsModel": "音声合成モデルID",
|
||||
"smseagleApiType": "API バージョン",
|
||||
"smseagleApiv1": "APIv1 (既存プロジェクトおよび下位互換性用)",
|
||||
"smseagleApiv2": "APIv2 (新規統合に推奨)",
|
||||
"smseagleComma": "複数指定する場合はコンマで区切ってください",
|
||||
"SpugPush Template Code": "テンプレートコード",
|
||||
"FlashDuty Push URL": "Push URL",
|
||||
"FlashDuty Push URL Placeholder": "アラート統合ページからコピー",
|
||||
"smtpHelpText": "SMTPSは『SMTP/TLSが機能しているかテスト』Ignore TLSは『プレーンテキストで接続』STARTTLSは『接続し、STARTTLSコマンドを発行し、サーバー証明書を検証』いずれもメールを送信しない。",
|
||||
"Custom URL": "カスタムURL",
|
||||
"customUrlDescription": "モニターのURLの代わりにクリック可能なURLとして使用されます。",
|
||||
"OneChatAccessToken": "OneChat アクセストークン"
|
||||
}
|
||||
|
@@ -1115,5 +1115,11 @@
|
||||
"Sender name": "Naam afzender",
|
||||
"smsplanetNeedToApproveName": "Moet worden goedgekeurd in het clientpaneel",
|
||||
"smsplanetApiToken": "Token voor de SMSPlanet API",
|
||||
"smsplanetApiDocs": "Gedetailleerde informatie over het verkrijgen van API-tokens vindt u op {the_smsplanet_documentation}."
|
||||
"smsplanetApiDocs": "Gedetailleerde informatie over het verkrijgen van API-tokens vindt u op {the_smsplanet_documentation}.",
|
||||
"defaultFriendlyName": "Nieuwe monitor",
|
||||
"Add Tags": "Labels toevoegen",
|
||||
"tagAlreadyOnMonitor": "Dit label (naam en waarde) is al op de monitor gekoppeld of in afwachting van koppelen.",
|
||||
"mqttWebsocketPathExplanation": "WebSocket pad voor MQTT via WebSocket verbindingen (bijv., /mqtt)",
|
||||
"mqttWebsocketPathInvalid": "Gebruik een geldig WebSocket pad formaat alsjeblieft",
|
||||
"mqttHostnameTip": "Gebruik dit formaat alsjeblieft {hostnaamFormat}"
|
||||
}
|
||||
|
@@ -1139,5 +1139,12 @@
|
||||
"Ip Family": "Família IP",
|
||||
"ipFamilyDescriptionAutoSelect": "Usa {happyEyeballs} para determinar a família IP.",
|
||||
"Optional: The audience to request the JWT for": "Opcional: O público deve solicitar o JWT para",
|
||||
"OAuth Audience": "Público OAuth"
|
||||
"OAuth Audience": "Público OAuth",
|
||||
"mqttWebSocketPath": "Caminho MQTT WebSocket",
|
||||
"mqttWebsocketPathExplanation": "Caminho do WebSocket para conexões MQTT sobre WebSocket (por exemplo, /mqtt)",
|
||||
"Path": "Caminho",
|
||||
"mqttWebsocketPathInvalid": "Use um formato de caminho WebSocket válido",
|
||||
"mqttHostnameTip": "Por favor, use este formato {hostnameFormat}",
|
||||
"issueWithGoogleChatOnAndroidHelptext": "Isso também permite contornar bugs no upstream, como {issuetackerURL}",
|
||||
"Template plain text instead of using cards": "Modelo de texto simples em vez de usar cartões"
|
||||
}
|
||||
|
@@ -192,8 +192,8 @@
|
||||
"statusPageNothing": "Não tem nada aqui, adicione um grupo os monitor.",
|
||||
"statusPageRefreshIn": "Atualize em: {0}",
|
||||
"No Services": "Sem serviços",
|
||||
"Partially Degraded Service": "Serviço parcialmente degradado",
|
||||
"Degraded Service": "Serviços degradados",
|
||||
"Partially Degraded Service": "Algumas funcionalidades estão indisponíveis",
|
||||
"Degraded Service": "Serviço indisponível",
|
||||
"Edit Status Page": "Editar página de status",
|
||||
"Go to Dashboard": "Ir para o painel de controle",
|
||||
"Status Page": "Status Page",
|
||||
|
@@ -29,7 +29,7 @@
|
||||
"Add New Monitor": "Новый монитор",
|
||||
"Quick Stats": "Сводка",
|
||||
"Up": "Доступен",
|
||||
"Down": "Не доступен",
|
||||
"Down": "Недоступен",
|
||||
"Pending": "В ожидании",
|
||||
"Unknown": "Неизвестно",
|
||||
"Pause": "Пауза",
|
||||
@@ -270,7 +270,7 @@
|
||||
"octopushLegacyHint": "Вы используете старую версию Octopush (2011-2020) или новую?",
|
||||
"Check octopush prices": "Тарифы Octopush {0}.",
|
||||
"octopushPhoneNumber": "Номер телефона (межд. формат, например: +79831234567) ",
|
||||
"octopushSMSSender": "Имя отправителя SMS: 3-11 символов алвафита, цифр и пробелов (a-zA-Z0-9)",
|
||||
"octopushSMSSender": "Имя отправителя SMS: 3-11 символов алфавита, цифр и пробелов (a-zA-Z0-9)",
|
||||
"LunaSea Device ID": "ID устройства LunaSea",
|
||||
"Apprise URL": "Ссылка Уведомления",
|
||||
"Example:": "Пример: {0}",
|
||||
@@ -681,8 +681,8 @@
|
||||
"smseagleRecipient": "Получатель(и) (если множество, должны быть разделены запятой)",
|
||||
"smseagleToken": "Токен доступа API",
|
||||
"smseagleUrl": "URL вашего SMSEagle устройства",
|
||||
"smseagleEncoding": "Отправить в юникоде",
|
||||
"smseaglePriority": "Приоритет сообщения (0-9, по умолчанию = 0)",
|
||||
"smseagleEncoding": "Отправить в юникоде (по умолчанию=GSM-7)",
|
||||
"smseaglePriority": "Приоритет сообщения (0-9, высший приоритет = 9)",
|
||||
"Server Address": "Адрес сервера",
|
||||
"Learn More": "Узнать больше",
|
||||
"topicExplanation": "MQTT топик для мониторинга",
|
||||
@@ -699,7 +699,7 @@
|
||||
"Clone": "Клонировать",
|
||||
"cloneOf": "Копия {0}",
|
||||
"notificationRegional": "Региональный",
|
||||
"Add New Tag": "Добавить тег",
|
||||
"Add New Tag": "Добавить новый тег",
|
||||
"Body Encoding": "Тип содержимого запроса.(JSON or XML)",
|
||||
"Strategy": "Стратегия",
|
||||
"Free Mobile User Identifier": "Бесплатный мобильный идентификатор пользователя",
|
||||
@@ -907,7 +907,7 @@
|
||||
"successKeyword": "Ключевое слово успеха",
|
||||
"statusPageSpecialSlugDesc": "Специальный ярлык {0}: эта страница будет отображаться, если ярлык не указан",
|
||||
"ntfyPriorityHelptextAllExceptDown": "Все события отправляются с этим приоритетом, кроме {0}-событий, которые имеют приоритет {1}",
|
||||
"self-hosted container": "контейнер, который хоститься самостоятельно",
|
||||
"self-hosted container": "контейнер, который хостится самостоятельно",
|
||||
"remoteBrowserToggle": "По умолчанию Chromium работает внутри контейнера Uptime Kuma. Вы можете использовать удаленный браузер, переключив этот переключатель.",
|
||||
"Remote Browsers": "Удаленные браузеры",
|
||||
"Remote Browser": "Удаленный браузер",
|
||||
@@ -1059,7 +1059,7 @@
|
||||
"Client ID": "ID клиента",
|
||||
"Client Secret": "Секрет клиента",
|
||||
"OAuth Scope": "Область действия OAuth",
|
||||
"Go back to home page.": "Вернутся на домашнюю страницу.",
|
||||
"Go back to home page.": "Вернуться на домашнюю страницу.",
|
||||
"No tags found.": "Теги не найдены.",
|
||||
"Cannot connect to the socket server.": "Невозможно подключиться к серверу сокетов.",
|
||||
"SIGNL4": "SIGNL4",
|
||||
@@ -1132,5 +1132,30 @@
|
||||
"the smsplanet documentation": "документация SMSPlanet",
|
||||
"Phone numbers": "Номера телефонов",
|
||||
"Sender name": "Имя отправителя",
|
||||
"smsplanetNeedToApproveName": "Требуется одобрение в панели клиента"
|
||||
"smsplanetNeedToApproveName": "Требуется одобрение в панели клиента",
|
||||
"Add Tags": "Добавить тег",
|
||||
"tagNameExists": "Тег с таким именем уже существует. Выберите его из списка или используйте другое имя.",
|
||||
"Use HTML for custom E-mail body": "Используйте HTML для пользовательского текста E-Mail",
|
||||
"Template plain text instead of using cards": "Шаблон простого текста вместо использования карт",
|
||||
"issueWithGoogleChatOnAndroidHelptext": "Это также позволяет обойти баги входящего потока, как {issuetakerURL}",
|
||||
"smseagleGroupV2": "Идентификатор группы Phonebook(s)",
|
||||
"smseagleApiv2": "APIv2 (рекомендовано для новых интеграций)",
|
||||
"smseagleApiv1": "APIv1 (для существующих проектов и обратной совместимости)",
|
||||
"Path": "Путь",
|
||||
"mqttWebSocketPath": "MQTT WebSocket путь",
|
||||
"mqttWebsocketPathExplanation": "WebSocket путь для MQTT через WebSocket соединение (например, /mqtt)",
|
||||
"mqttWebsocketPathInvalid": "Пожалуйста, используйте правильный формат пути WebSocket",
|
||||
"mqttHostnameTip": "Пожалуйста, используйте этот формат {hostnameFormat}",
|
||||
"smseagleApiType": "Версия API",
|
||||
"smseagleComma": "Множественные должны быть разделены запятой",
|
||||
"smseagleDocs": "Просмотрите документацию или доступность APIv2: {0}",
|
||||
"smseagleTtsModel": "Модель текст-в-речь ID",
|
||||
"smseagleDuration": "Длительность (в секундах)",
|
||||
"smseagleMsgType": "Сообщение типа",
|
||||
"smseagleMsgSms": "SMS сообщение (по умолчанию)",
|
||||
"smseagleMsgTts": "Звонок текст-в-речь",
|
||||
"smseagleMsgTtsAdvanced": "Текст-в-речь расширенный звонок",
|
||||
"defaultFriendlyName": "Новый монитор",
|
||||
"tagAlreadyOnMonitor": "Этот тег (имя и значение) уже отображается на мониторе или ожидает добавления.",
|
||||
"tagAlreadyStaged": "Этот тег (имя и значение) уже подготовлен для этого пакета."
|
||||
}
|
||||
|
@@ -990,5 +990,13 @@
|
||||
"OID (Object Identifier)": "OID (Objektsidentifierare)",
|
||||
"Please enter a valid OID.": "Ange ett giltigt OID.",
|
||||
"Group ID": "GruppID",
|
||||
"Add Remote Browser": "Lägg till fjärrbläddrare"
|
||||
"Add Remote Browser": "Lägg till fjärrbläddrare",
|
||||
"Path": "Sökväg",
|
||||
"mqttHostnameTip": "Vänligen använd detta format {hostnameFormat}",
|
||||
"templateServiceName": "tjänstnamn",
|
||||
"templateHostnameOrURL": "värdnamn eller URL",
|
||||
"templateStatus": "status",
|
||||
"defaultFriendlyName": "Ny övervakare",
|
||||
"Add Tags": "Lägg till taggar",
|
||||
"tagAlreadyOnMonitor": "Denna tagg (namn och värde) finns redan på övervakaren eller är köad för tillägg."
|
||||
}
|
||||
|
@@ -41,7 +41,7 @@
|
||||
"Check Update On GitHub": "GitHub'da Güncellemeyi Kontrol Edin",
|
||||
"List": "Liste",
|
||||
"Add": "Ekle",
|
||||
"Add New Monitor": "Yeni Servis Ekle",
|
||||
"Add New Monitor": "Yeni Monitör Ekle",
|
||||
"Quick Stats": "Servis istatistikleri",
|
||||
"Up": "Normal",
|
||||
"Down": "Hatalı",
|
||||
@@ -96,15 +96,15 @@
|
||||
"Timezone": "Zaman Dilimi",
|
||||
"Search Engine Visibility": "Arama Motoru Görünürlüğü",
|
||||
"Allow indexing": "İndekslemeye izin ver",
|
||||
"Discourage search engines from indexing site": "İndekslemeyi reddet",
|
||||
"Discourage search engines from indexing site": "Arama motorlarının siteyi indekslemesini engelleyin",
|
||||
"Change Password": "Şifre Değiştir",
|
||||
"Current Password": "Şuan ki Şifre",
|
||||
"Current Password": "Mevcut Şifre",
|
||||
"New Password": "Yeni Şifre",
|
||||
"Repeat New Password": "Yeni Şifreyi Tekrar Girin",
|
||||
"Update Password": "Şifreyi Değiştir",
|
||||
"Disable Auth": "Şifreli girişi iptal et",
|
||||
"Enable Auth": "Şifreli girişi aktif et",
|
||||
"disableauth.message1": "{disableAuth}emin misiniz?",
|
||||
"disableauth.message1": "{disableAuth} emin misiniz?",
|
||||
"disable authentication": "Şifreli girişi devre dışı bırakmak istediğinizden",
|
||||
"disableauth.message2": "Bu, Uptime Kuma'nın önünde Cloudflare Access gibi {intendThirdPartyAuth} kişiler içindir.",
|
||||
"where you intend to implement third-party authentication": "üçüncü taraf yetkilendirmesi olan",
|
||||
@@ -293,7 +293,7 @@
|
||||
"matrixHomeserverURL": "Homeserver URL (http(s):// ve isteğe bağlı olarak bağlantı noktası ile)",
|
||||
"Internal Room Id": "Internal Room ID",
|
||||
"matrixDesc1": "Internal Room ID'sini, Matrix istemcinizdeki oda ayarlarının gelişmiş bölümüne bakarak bulabilirsiniz. !QMdRCpUIfLwsfjxye6:home.server gibi görünmelidir.",
|
||||
"matrixDesc2": "Hesabınıza ve katıldığınız tüm odalara tam erişime izin vereceğinden, yeni bir kullanıcı oluşturmanız ve kendi Matrix kullanıcınızın erişim belirtecini kullanmamanız şiddetle tavsiye edilir. Bunun yerine, yeni bir kullanıcı oluşturun ve onu yalnızca bildirimi almak istediğiniz odaya davet edin. {0} komutunu çalıştırarak erişim tokenini alabilirsiniz.",
|
||||
"matrixDesc2": "Hesabınıza ve katıldığınız tüm odalara tam erişime izin vereceğinden, yeni bir kullanıcı oluşturmanız ve kendi Matrix kullanıcınızın erişim belirtecini kullanmamanız şiddetle tavsiye edilir. Bunun yerine, yeni bir kullanıcı oluşturun ve onu yalnızca bildirimi almak istediğiniz odaya davet edin. {0} komutunu çalıştırarak erişim tokenini alabilirsiniz",
|
||||
"Method": "Yöntem",
|
||||
"Body": "Gövde",
|
||||
"Headers": "Başlıklar",
|
||||
|
@@ -1181,5 +1181,17 @@
|
||||
"Ip Family": "Сімейство IP",
|
||||
"ipFamilyDescriptionAutoSelect": "Використовує {happyEyeballs} для визначення сімейства IP.",
|
||||
"OAuth Audience": "Аудиторія OAuth",
|
||||
"Optional: The audience to request the JWT for": "Необов'язково: Аудиторія, для якої необхідно подати запит на JWT"
|
||||
"Optional: The audience to request the JWT for": "Необов'язково: Аудиторія, для якої необхідно подати запит на JWT",
|
||||
"mqttWebsocketPathExplanation": "Шлях WebSocket для з'єднань MQTT через WebSocket (наприклад, /mqtt)",
|
||||
"Path": "Шлях",
|
||||
"mqttWebSocketPath": "Шлях до MQTT WebSocket",
|
||||
"mqttWebsocketPathInvalid": "Будь ласка, використовуйте дійсний формат шляху WebSocket",
|
||||
"mqttHostnameTip": "Будь ласка, використовуйте цей формат {hostnameFormat}",
|
||||
"Template plain text instead of using cards": "Шаблон простого тексту замість використання карток",
|
||||
"issueWithGoogleChatOnAndroidHelptext": "Це також дозволяє обійти помилки на кшталт {issuetackerURL}",
|
||||
"Clear All Events": "Очистити всі події",
|
||||
"Events cleared successfully": "Події успішно очищено.",
|
||||
"No monitors found": "Моніторів не знайдено.",
|
||||
"Could not clear events": "Не вдалося очистити події {failed}/{total}",
|
||||
"clearAllEventsMsg": "Ви дійсно хочете видалити всі події?"
|
||||
}
|
||||
|
@@ -1177,5 +1177,10 @@
|
||||
"smseagleMsgRing": "响铃呼叫",
|
||||
"smseagleMsgSms": "SMS 短信(默认值)",
|
||||
"smseagleContactV2": "联系人 ID",
|
||||
"smseagleGroupV2": "群组 ID"
|
||||
"smseagleGroupV2": "群组 ID",
|
||||
"mqttWebsocketPathExplanation": "MQTT 通过 WebSocket 连接时所使用的 WebSocket 路径(例如 /mqtt)",
|
||||
"Path": "路径",
|
||||
"mqttHostnameTip": "请使用此格式:{hostnameFormat}",
|
||||
"mqttWebsocketPathInvalid": "请使用合法的 WebSocket 路径格式",
|
||||
"mqttWebSocketPath": "MQTT WebSocket 路径"
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { currentLocale } from "../i18n";
|
||||
import { setPageLocale } from "../util-frontend";
|
||||
import { setPageLocale, relativeTimeFormatter } from "../util-frontend";
|
||||
const langModules = import.meta.glob("../lang/*.json");
|
||||
|
||||
export default {
|
||||
@@ -28,11 +28,13 @@ export default {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async changeLang(lang) {
|
||||
let message = (await langModules["../lang/" + lang + ".json"]()).default;
|
||||
let message = (await langModules["../lang/" + lang + ".json"]())
|
||||
.default;
|
||||
this.$i18n.setLocaleMessage(lang, message);
|
||||
this.$i18n.locale = lang;
|
||||
localStorage.locale = lang;
|
||||
setPageLocale();
|
||||
}
|
||||
}
|
||||
relativeTimeFormatter.updateLocale(lang);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@@ -46,6 +46,15 @@
|
||||
</div>
|
||||
|
||||
<div class="shadow-box table-shadow-box" style="overflow-x: hidden;">
|
||||
<div class="mb-3 text-end">
|
||||
<button
|
||||
class="btn btn-sm btn-outline-danger"
|
||||
:disabled="clearingAllEvents"
|
||||
@click="clearAllEventsDialog"
|
||||
>
|
||||
{{ $t("Clear All Events") }}
|
||||
</button>
|
||||
</div>
|
||||
<table class="table table-borderless table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -82,6 +91,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<Confirm
|
||||
ref="confirmClearEvents"
|
||||
btn-style="btn-danger"
|
||||
:yes-text="$t('Yes')"
|
||||
:no-text="$t('No')"
|
||||
@yes="clearAllEvents"
|
||||
>
|
||||
{{ $t("clearAllEventsMsg") }}
|
||||
</Confirm>
|
||||
<router-view ref="child" />
|
||||
</template>
|
||||
|
||||
@@ -89,12 +107,14 @@
|
||||
import Status from "../components/Status.vue";
|
||||
import Datetime from "../components/Datetime.vue";
|
||||
import Pagination from "v-pagination-3";
|
||||
import Confirm from "../components/Confirm.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Datetime,
|
||||
Status,
|
||||
Pagination,
|
||||
Confirm,
|
||||
},
|
||||
props: {
|
||||
calculatedHeight: {
|
||||
@@ -113,6 +133,7 @@ export default {
|
||||
},
|
||||
importantHeartBeatListLength: 0,
|
||||
displayedRecords: [],
|
||||
clearingAllEvents: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
@@ -203,6 +224,43 @@ export default {
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
clearAllEventsDialog() {
|
||||
this.$refs.confirmClearEvents.show();
|
||||
},
|
||||
clearAllEvents() {
|
||||
this.clearingAllEvents = true;
|
||||
const monitorIDs = Object.keys(this.$root.monitorList);
|
||||
let failed = 0;
|
||||
const total = monitorIDs.length;
|
||||
|
||||
if (total === 0) {
|
||||
this.clearingAllEvents = false;
|
||||
this.$root.toastError(this.$t("No monitors found"));
|
||||
return;
|
||||
}
|
||||
|
||||
monitorIDs.forEach((monitorID) => {
|
||||
this.$root.getSocket().emit("clearEvents", monitorID, (res) => {
|
||||
if (!res || !res.ok) {
|
||||
failed++;
|
||||
}
|
||||
});
|
||||
});
|
||||
this.clearingAllEvents = false;
|
||||
this.page = 1;
|
||||
this.getImportantHeartbeatListLength();
|
||||
if (failed === 0) {
|
||||
this.$root.toastSuccess(this.$t("Events cleared successfully"));
|
||||
} else {
|
||||
this.$root.toastError(
|
||||
this.$t("Could not clear events", {
|
||||
failed,
|
||||
total,
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -246,3 +304,4 @@ table {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
@@ -1,7 +1,9 @@
|
||||
<template>
|
||||
<transition name="slide-fade" appear>
|
||||
<div v-if="monitor">
|
||||
<router-link v-if="group !== ''" :to="monitorURL(monitor.parent)"> {{ group }}</router-link>
|
||||
<router-link v-if="group !== ''" :to="monitorURL(monitor.parent)">
|
||||
{{ group }}
|
||||
</router-link>
|
||||
<h1>
|
||||
{{ monitor.name }}
|
||||
<div class="monitor-id">
|
||||
@@ -13,61 +15,124 @@
|
||||
<p v-if="monitor.description" v-html="descriptionHTML"></p>
|
||||
<div class="d-flex">
|
||||
<div class="tags">
|
||||
<Tag v-for="tag in monitor.tags" :key="tag.id" :item="tag" :size="'sm'" />
|
||||
<Tag
|
||||
v-for="tag in monitor.tags"
|
||||
:key="tag.id"
|
||||
:item="tag"
|
||||
:size="'sm'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="url">
|
||||
<a v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'mp-health' || monitor.type === 'real-browser' " :href="monitor.url" target="_blank" rel="noopener noreferrer">{{ filterPassword(monitor.url) }}</a>
|
||||
<a
|
||||
v-if="
|
||||
monitor.type === 'http' ||
|
||||
monitor.type === 'keyword' ||
|
||||
monitor.type === 'json-query' ||
|
||||
monitor.type === 'mp-health' ||
|
||||
monitor.type === 'real-browser'
|
||||
"
|
||||
:href="monitor.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>{{ filterPassword(monitor.url) }}</a>
|
||||
<span v-if="monitor.type === 'port'">TCP Port {{ monitor.hostname }}:{{ monitor.port }}</span>
|
||||
<span v-if="monitor.type === 'ping'">Ping: {{ monitor.hostname }}</span>
|
||||
<span v-if="monitor.type === 'keyword'">
|
||||
<br>
|
||||
<br />
|
||||
<span>{{ $t("Keyword") }}: </span>
|
||||
<span class="keyword">{{ monitor.keyword }}</span>
|
||||
<span v-if="monitor.invertKeyword" alt="Inverted keyword" class="keyword-inverted"> ↧</span>
|
||||
<span
|
||||
v-if="monitor.invertKeyword"
|
||||
alt="Inverted keyword"
|
||||
class="keyword-inverted"
|
||||
>
|
||||
↧</span>
|
||||
</span>
|
||||
<span v-if="monitor.type === 'json-query'">
|
||||
<br>
|
||||
<span>{{ $t("Json Query") }}:</span> <span class="keyword">{{ monitor.jsonPath }}</span>
|
||||
<br>
|
||||
<span>{{ $t("Expected Value") }}:</span> <span class="keyword">{{ monitor.expectedValue }}</span>
|
||||
<br />
|
||||
<span>{{ $t("Json Query") }}:</span>
|
||||
<span class="keyword">{{ monitor.jsonPath }}</span>
|
||||
<br />
|
||||
<span>{{ $t("Expected Value") }}:</span>
|
||||
<span class="keyword">{{ monitor.expectedValue }}</span>
|
||||
</span>
|
||||
<span v-if="monitor.type === 'dns'">[{{ monitor.dns_resolve_type }}] {{ monitor.hostname }}
|
||||
<br>
|
||||
<span>{{ $t("Last Result") }}:</span> <span class="keyword">{{ monitor.dns_last_result }}</span>
|
||||
<br />
|
||||
<span>{{ $t("Last Result") }}:</span>
|
||||
<span class="keyword">{{ monitor.dns_last_result }}</span>
|
||||
</span>
|
||||
<span v-if="monitor.type === 'docker'">Docker container: {{ monitor.docker_container }}</span>
|
||||
<span v-if="monitor.type === 'gamedig'">Gamedig - {{ monitor.game }}: {{ monitor.hostname }}:{{ monitor.port }}</span>
|
||||
<span v-if="monitor.type === 'gamedig'">Gamedig - {{ monitor.game }}: {{ monitor.hostname }}:{{
|
||||
monitor.port
|
||||
}}</span>
|
||||
<span v-if="monitor.type === 'grpc-keyword'">gRPC - {{ filterPassword(monitor.grpcUrl) }}
|
||||
<br>
|
||||
<span>{{ $t("Keyword") }}:</span> <span class="keyword">{{ monitor.keyword }}</span>
|
||||
<br />
|
||||
<span>{{ $t("Keyword") }}:</span>
|
||||
<span class="keyword">{{ monitor.keyword }}</span>
|
||||
</span>
|
||||
<span v-if="monitor.type === 'mongodb'">{{ filterPassword(monitor.databaseConnectionString) }}</span>
|
||||
<span v-if="monitor.type === 'mqtt'">MQTT: {{ monitor.hostname }}:{{ monitor.port }}/{{ monitor.mqttTopic }}</span>
|
||||
<span v-if="monitor.type === 'mysql'">{{ filterPassword(monitor.databaseConnectionString) }}</span>
|
||||
<span v-if="monitor.type === 'postgres'">{{ filterPassword(monitor.databaseConnectionString) }}</span>
|
||||
<span v-if="monitor.type === 'push'">Push: <a :href="pushURL" target="_blank" rel="noopener noreferrer">{{ pushURL }}</a></span>
|
||||
<span v-if="monitor.type === 'mongodb'">{{
|
||||
filterPassword(monitor.databaseConnectionString)
|
||||
}}</span>
|
||||
<span v-if="monitor.type === 'mqtt'">MQTT: {{ monitor.hostname }}:{{ monitor.port }}/{{
|
||||
monitor.mqttTopic
|
||||
}}</span>
|
||||
<span v-if="monitor.type === 'mysql'">{{
|
||||
filterPassword(monitor.databaseConnectionString)
|
||||
}}</span>
|
||||
<span v-if="monitor.type === 'postgres'">{{
|
||||
filterPassword(monitor.databaseConnectionString)
|
||||
}}</span>
|
||||
<span v-if="monitor.type === 'push'">Push:
|
||||
<a
|
||||
:href="pushURL"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>{{ pushURL }}</a></span>
|
||||
<span v-if="monitor.type === 'radius'">Radius: {{ filterPassword(monitor.hostname) }}</span>
|
||||
<span v-if="monitor.type === 'redis'">{{ filterPassword(monitor.databaseConnectionString) }}</span>
|
||||
<span v-if="monitor.type === 'sqlserver'">SQL Server: {{ filterPassword(monitor.databaseConnectionString) }}</span>
|
||||
<span v-if="monitor.type === 'steam'">Steam Game Server: {{ monitor.hostname }}:{{ monitor.port }}</span>
|
||||
<span v-if="monitor.type === 'redis'">{{
|
||||
filterPassword(monitor.databaseConnectionString)
|
||||
}}</span>
|
||||
<span v-if="monitor.type === 'sqlserver'">SQL Server:
|
||||
{{ filterPassword(monitor.databaseConnectionString) }}</span>
|
||||
<span v-if="monitor.type === 'steam'">Steam Game Server: {{ monitor.hostname }}:{{
|
||||
monitor.port
|
||||
}}</span>
|
||||
</p>
|
||||
|
||||
<div class="functions">
|
||||
<div class="btn-group" role="group">
|
||||
<button v-if="monitor.active" class="btn btn-normal" @click="pauseDialog">
|
||||
<button
|
||||
v-if="monitor.active"
|
||||
class="btn btn-normal"
|
||||
@click="pauseDialog"
|
||||
>
|
||||
<font-awesome-icon icon="pause" /> {{ $t("Pause") }}
|
||||
</button>
|
||||
<button v-if="! monitor.active" class="btn btn-primary" :disabled="monitor.forceInactive" @click="resumeMonitor">
|
||||
<button
|
||||
v-if="!monitor.active"
|
||||
class="btn btn-primary"
|
||||
:disabled="monitor.forceInactive"
|
||||
@click="resumeMonitor"
|
||||
>
|
||||
<font-awesome-icon icon="play" /> {{ $t("Resume") }}
|
||||
</button>
|
||||
<router-link :to=" '/edit/' + monitor.id " class="btn btn-normal">
|
||||
<router-link
|
||||
:to="'/edit/' + monitor.id"
|
||||
class="btn btn-normal"
|
||||
>
|
||||
<font-awesome-icon icon="edit" /> {{ $t("Edit") }}
|
||||
</router-link>
|
||||
<router-link :to=" '/clone/' + monitor.id " class="btn btn-normal">
|
||||
<router-link
|
||||
:to="'/clone/' + monitor.id"
|
||||
class="btn btn-normal"
|
||||
>
|
||||
<font-awesome-icon icon="clone" /> {{ $t("Clone") }}
|
||||
</router-link>
|
||||
<button class="btn btn-normal text-danger" @click="deleteDialog">
|
||||
<button
|
||||
class="btn btn-normal text-danger"
|
||||
@click="deleteDialog"
|
||||
>
|
||||
<font-awesome-icon icon="trash" /> {{ $t("Delete") }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -77,29 +142,53 @@
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<HeartbeatBar :monitor-id="monitor.id" />
|
||||
<span class="word">{{ $t("checkEverySecond", [ monitor.interval ]) }}</span>
|
||||
<span class="word">{{
|
||||
$t("checkEverySecond", [monitor.interval])
|
||||
}}
|
||||
({{
|
||||
secondsToHumanReadableFormat(monitor.interval)
|
||||
}})</span>
|
||||
</div>
|
||||
<div class="col-md-4 text-center">
|
||||
<span class="badge rounded-pill" :class=" 'bg-' + status.color " style="font-size: 30px;" data-testid="monitor-status">{{ status.text }}</span>
|
||||
<span
|
||||
class="badge rounded-pill"
|
||||
:class="'bg-' + status.color"
|
||||
style="font-size: 30px;"
|
||||
data-testid="monitor-status"
|
||||
>{{ status.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Push Examples -->
|
||||
<div v-if="monitor.type === 'push'" class="shadow-box big-padding">
|
||||
<a href="#" @click="pushMonitor.showPushExamples = !pushMonitor.showPushExamples">{{ $t("pushViewCode") }}</a>
|
||||
<a
|
||||
href="#"
|
||||
@click="
|
||||
pushMonitor.showPushExamples =
|
||||
!pushMonitor.showPushExamples
|
||||
"
|
||||
>{{ $t("pushViewCode") }}</a>
|
||||
|
||||
<transition name="slide-fade" appear>
|
||||
<div v-if="pushMonitor.showPushExamples" class="mt-3">
|
||||
<select id="push-current-example" v-model="pushMonitor.currentExample" class="form-select">
|
||||
<select
|
||||
id="push-current-example"
|
||||
v-model="pushMonitor.currentExample"
|
||||
class="form-select"
|
||||
>
|
||||
<optgroup :label="$t('programmingLanguages')">
|
||||
<option value="csharp">C#</option>
|
||||
<option value="go">Go</option>
|
||||
<option value="java">Java</option>
|
||||
<option value="javascript-fetch">JavaScript (fetch)</option>
|
||||
<option value="javascript-fetch">
|
||||
JavaScript (fetch)
|
||||
</option>
|
||||
<option value="php">PHP</option>
|
||||
<option value="python">Python</option>
|
||||
<option value="typescript-fetch">TypeScript (fetch)</option>
|
||||
<option value="typescript-fetch">
|
||||
TypeScript (fetch)
|
||||
</option>
|
||||
</optgroup>
|
||||
<optgroup :label="$t('pushOthers')">
|
||||
<option value="bash-curl">Bash (curl)</option>
|
||||
@@ -108,7 +197,13 @@
|
||||
</optgroup>
|
||||
</select>
|
||||
|
||||
<prism-editor v-model="pushMonitor.code" class="css-editor mt-3" :highlight="pushExampleHighlighter" line-numbers readonly></prism-editor>
|
||||
<prism-editor
|
||||
v-model="pushMonitor.code"
|
||||
class="css-editor mt-3"
|
||||
:highlight="pushExampleHighlighter"
|
||||
line-numbers
|
||||
readonly
|
||||
></prism-editor>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
@@ -116,55 +211,98 @@
|
||||
<!-- 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">
|
||||
<div
|
||||
v-if="monitor.type !== 'group'"
|
||||
class="col-12 col-sm col row d-flex align-items-center d-sm-block"
|
||||
>
|
||||
<h4 class="col-4 col-sm-12">{{ pingTitle() }}</h4>
|
||||
<p class="col-4 col-sm-12 mb-0 mb-sm-2">({{ $t("Current") }})</p>
|
||||
<p class="col-4 col-sm-12 mb-0 mb-sm-2">
|
||||
({{ $t("Current") }})
|
||||
</p>
|
||||
<span class="col-4 col-sm-12 num">
|
||||
<a href="#" @click.prevent="showPingChartBox = !showPingChartBox">
|
||||
<a
|
||||
href="#"
|
||||
@click.prevent="
|
||||
showPingChartBox = !showPingChartBox
|
||||
"
|
||||
>
|
||||
<CountUp :value="ping" />
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="monitor.type !== 'group'" class="col-12 col-sm col row d-flex align-items-center d-sm-block">
|
||||
<div
|
||||
v-if="monitor.type !== 'group'"
|
||||
class="col-12 col-sm col row d-flex align-items-center d-sm-block"
|
||||
>
|
||||
<h4 class="col-4 col-sm-12">{{ pingTitle(true) }}</h4>
|
||||
<p class="col-4 col-sm-12 mb-0 mb-sm-2">(24{{ $t("-hour") }})</p>
|
||||
<p class="col-4 col-sm-12 mb-0 mb-sm-2">
|
||||
(24{{ $t("-hour") }})
|
||||
</p>
|
||||
<span class="col-4 col-sm-12 num">
|
||||
<CountUp :value="avgPing" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Uptime (24-hour) -->
|
||||
<div class="col-12 col-sm col row d-flex align-items-center d-sm-block">
|
||||
<div
|
||||
class="col-12 col-sm col row d-flex align-items-center d-sm-block"
|
||||
>
|
||||
<h4 class="col-4 col-sm-12">{{ $t("Uptime") }}</h4>
|
||||
<p class="col-4 col-sm-12 mb-0 mb-sm-2">(24{{ $t("-hour") }})</p>
|
||||
<p class="col-4 col-sm-12 mb-0 mb-sm-2">
|
||||
(24{{ $t("-hour") }})
|
||||
</p>
|
||||
<span class="col-4 col-sm-12 num">
|
||||
<Uptime :monitor="monitor" type="24" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Uptime (30-day) -->
|
||||
<div class="col-12 col-sm col row d-flex align-items-center d-sm-block">
|
||||
<div
|
||||
class="col-12 col-sm col row d-flex align-items-center d-sm-block"
|
||||
>
|
||||
<h4 class="col-4 col-sm-12">{{ $t("Uptime") }}</h4>
|
||||
<p class="col-4 col-sm-12 mb-0 mb-sm-2">(30{{ $t("-day") }})</p>
|
||||
<p class="col-4 col-sm-12 mb-0 mb-sm-2">
|
||||
(30{{ $t("-day") }})
|
||||
</p>
|
||||
<span class="col-4 col-sm-12 num">
|
||||
<Uptime :monitor="monitor" type="720" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Uptime (1-year) -->
|
||||
<div class="col-12 col-sm col row d-flex align-items-center d-sm-block">
|
||||
<div
|
||||
class="col-12 col-sm col row d-flex align-items-center d-sm-block"
|
||||
>
|
||||
<h4 class="col-4 col-sm-12">{{ $t("Uptime") }}</h4>
|
||||
<p class="col-4 col-sm-12 mb-0 mb-sm-2">(1{{ $t("-year") }})</p>
|
||||
<p class="col-4 col-sm-12 mb-0 mb-sm-2">
|
||||
(1{{ $t("-year") }})
|
||||
</p>
|
||||
<span class="col-4 col-sm-12 num">
|
||||
<Uptime :monitor="monitor" type="1y" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="tlsInfo" class="col-12 col-sm col row d-flex align-items-center d-sm-block">
|
||||
<div
|
||||
v-if="tlsInfo"
|
||||
class="col-12 col-sm col row d-flex align-items-center d-sm-block"
|
||||
>
|
||||
<h4 class="col-4 col-sm-12">{{ $t("Cert Exp.") }}</h4>
|
||||
<p class="col-4 col-sm-12 mb-0 mb-sm-2">(<Datetime :value="tlsInfo.certInfo.validTo" date-only />)</p>
|
||||
<p class="col-4 col-sm-12 mb-0 mb-sm-2">
|
||||
(<Datetime
|
||||
:value="tlsInfo.certInfo.validTo"
|
||||
date-only
|
||||
/>)
|
||||
</p>
|
||||
<span class="col-4 col-sm-12 num">
|
||||
<a href="#" @click.prevent="toggleCertInfoBox = !toggleCertInfoBox">{{ tlsInfo.certInfo.daysRemaining }} {{ $tc("day", tlsInfo.certInfo.daysRemaining) }}</a>
|
||||
<a
|
||||
href="#"
|
||||
@click.prevent="
|
||||
toggleCertInfoBox = !toggleCertInfoBox
|
||||
"
|
||||
>{{ tlsInfo.certInfo.daysRemaining }}
|
||||
{{
|
||||
$tc("day", tlsInfo.certInfo.daysRemaining)
|
||||
}}</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -172,17 +310,26 @@
|
||||
|
||||
<!-- Cert Info Box -->
|
||||
<transition name="slide-fade" appear>
|
||||
<div v-if="showCertInfoBox" class="shadow-box big-padding text-center">
|
||||
<div
|
||||
v-if="showCertInfoBox"
|
||||
class="shadow-box big-padding text-center"
|
||||
>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<certificate-info :certInfo="tlsInfo.certInfo" :valid="tlsInfo.valid" />
|
||||
<certificate-info
|
||||
:certInfo="tlsInfo.certInfo"
|
||||
:valid="tlsInfo.valid"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<!-- Ping Chart -->
|
||||
<div v-if="showPingChartBox" class="shadow-box big-padding text-center ping-chart-wrapper">
|
||||
<div
|
||||
v-if="showPingChartBox"
|
||||
class="shadow-box big-padding text-center ping-chart-wrapper"
|
||||
>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<PingChart :monitor-id="monitor.id" />
|
||||
@@ -194,25 +341,46 @@
|
||||
<div v-if="monitor.type === 'real-browser'" class="shadow-box">
|
||||
<div class="row">
|
||||
<div class="col-md-6 zoom-cursor">
|
||||
<img :src="screenshotURL" style="width: 100%;" alt="screenshot of the website" @click="showScreenshotDialog">
|
||||
<img
|
||||
:src="screenshotURL"
|
||||
style="width: 100%;"
|
||||
alt="screenshot of the website"
|
||||
@click="showScreenshotDialog"
|
||||
/>
|
||||
</div>
|
||||
<ScreenshotDialog ref="screenshotDialog" :imageURL="screenshotURL" />
|
||||
<ScreenshotDialog
|
||||
ref="screenshotDialog"
|
||||
:imageURL="screenshotURL"
|
||||
/>
|
||||
</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">
|
||||
<font-awesome-icon icon="trash" /> {{ $t("Clear Data") }}
|
||||
<button
|
||||
class="btn btn-sm btn-outline-danger dropdown-toggle"
|
||||
type="button"
|
||||
data-bs-toggle="dropdown"
|
||||
>
|
||||
<font-awesome-icon icon="trash" />
|
||||
{{ $t("Clear Data") }}
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<button type="button" class="dropdown-item" @click="clearEventsDialog">
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item"
|
||||
@click="clearEventsDialog"
|
||||
>
|
||||
{{ $t("Events") }}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item" @click="clearHeartbeatsDialog">
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item"
|
||||
@click="clearHeartbeatsDialog"
|
||||
>
|
||||
{{ $t("Heartbeats") }}
|
||||
</button>
|
||||
</li>
|
||||
@@ -227,9 +395,15 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(beat, index) in displayedRecords" :key="index" style="padding: 10px;">
|
||||
<tr
|
||||
v-for="(beat, index) in displayedRecords"
|
||||
:key="index"
|
||||
style="padding: 10px;"
|
||||
>
|
||||
<td><Status :status="beat.status" /></td>
|
||||
<td :class="{ 'border-0':! beat.msg}"><Datetime :value="beat.time" /></td>
|
||||
<td :class="{ 'border-0': !beat.msg }">
|
||||
<Datetime :value="beat.time" />
|
||||
</td>
|
||||
<td class="border-0">{{ beat.msg }}</td>
|
||||
</tr>
|
||||
|
||||
@@ -251,19 +425,42 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Confirm ref="confirmPause" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="pauseMonitor">
|
||||
<Confirm
|
||||
ref="confirmPause"
|
||||
:yes-text="$t('Yes')"
|
||||
:no-text="$t('No')"
|
||||
@yes="pauseMonitor"
|
||||
>
|
||||
{{ $t("pauseMonitorMsg") }}
|
||||
</Confirm>
|
||||
|
||||
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteMonitor">
|
||||
<Confirm
|
||||
ref="confirmDelete"
|
||||
btn-style="btn-danger"
|
||||
:yes-text="$t('Yes')"
|
||||
:no-text="$t('No')"
|
||||
@yes="deleteMonitor"
|
||||
>
|
||||
{{ $t("deleteMonitorMsg") }}
|
||||
</Confirm>
|
||||
|
||||
<Confirm ref="confirmClearEvents" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="clearEvents">
|
||||
<Confirm
|
||||
ref="confirmClearEvents"
|
||||
btn-style="btn-danger"
|
||||
:yes-text="$t('Yes')"
|
||||
:no-text="$t('No')"
|
||||
@yes="clearEvents"
|
||||
>
|
||||
{{ $t("clearEventsMsg") }}
|
||||
</Confirm>
|
||||
|
||||
<Confirm ref="confirmClearHeartbeats" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="clearHeartbeats">
|
||||
<Confirm
|
||||
ref="confirmClearHeartbeats"
|
||||
btn-style="btn-danger"
|
||||
:yes-text="$t('Yes')"
|
||||
:no-text="$t('No')"
|
||||
@yes="clearHeartbeats"
|
||||
>
|
||||
{{ $t("clearHeartbeatsMsg") }}
|
||||
</Confirm>
|
||||
</div>
|
||||
@@ -281,14 +478,16 @@ import Datetime from "../components/Datetime.vue";
|
||||
import CountUp from "../components/CountUp.vue";
|
||||
import Uptime from "../components/Uptime.vue";
|
||||
import Pagination from "v-pagination-3";
|
||||
const PingChart = defineAsyncComponent(() => import("../components/PingChart.vue"));
|
||||
const PingChart = defineAsyncComponent(() =>
|
||||
import("../components/PingChart.vue")
|
||||
);
|
||||
import Tag from "../components/Tag.vue";
|
||||
import CertificateInfo from "../components/CertificateInfo.vue";
|
||||
import { getMonitorRelativeURL } from "../util.ts";
|
||||
import { URL } from "whatwg-url";
|
||||
import DOMPurify from "dompurify";
|
||||
import { marked } from "marked";
|
||||
import { getResBaseURL } from "../util-frontend";
|
||||
import { getResBaseURL, relativeTimeFormatter } from "../util-frontend";
|
||||
import { highlight, languages } from "prismjs/components/prism-core";
|
||||
import "prismjs/components/prism-clike";
|
||||
import "prismjs/components/prism-javascript";
|
||||
@@ -310,7 +509,7 @@ export default {
|
||||
Tag,
|
||||
CertificateInfo,
|
||||
PrismEditor,
|
||||
ScreenshotDialog
|
||||
ScreenshotDialog,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -344,7 +543,10 @@ export default {
|
||||
// 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]) {
|
||||
if (
|
||||
this.monitor.id in this.$root.lastHeartbeatList &&
|
||||
this.$root.lastHeartbeatList[this.monitor.id]
|
||||
) {
|
||||
return this.$root.lastHeartbeatList[this.monitor.id];
|
||||
}
|
||||
|
||||
@@ -362,7 +564,10 @@ export default {
|
||||
},
|
||||
|
||||
avgPing() {
|
||||
if (this.$root.avgPingList[this.monitor.id] || this.$root.avgPingList[this.monitor.id] === 0) {
|
||||
if (
|
||||
this.$root.avgPingList[this.monitor.id] ||
|
||||
this.$root.avgPingList[this.monitor.id] === 0
|
||||
) {
|
||||
return this.$root.avgPingList[this.monitor.id];
|
||||
}
|
||||
|
||||
@@ -374,14 +579,17 @@ export default {
|
||||
return this.$root.statusList[this.monitor.id];
|
||||
}
|
||||
|
||||
return { };
|
||||
return {};
|
||||
},
|
||||
|
||||
tlsInfo() {
|
||||
// Add: this.$root.tlsInfoList[this.monitor.id].certInfo
|
||||
// Fix: TypeError: Cannot read properties of undefined (reading 'validTo')
|
||||
// Reason: TLS Info object format is changed in 1.8.0, if for some reason, it cannot connect to the site after update to 1.8.0, the object is still in the old format.
|
||||
if (this.$root.tlsInfoList[this.monitor.id] && this.$root.tlsInfoList[this.monitor.id].certInfo) {
|
||||
if (
|
||||
this.$root.tlsInfoList[this.monitor.id] &&
|
||||
this.$root.tlsInfoList[this.monitor.id].certInfo
|
||||
) {
|
||||
return this.$root.tlsInfoList[this.monitor.id];
|
||||
}
|
||||
|
||||
@@ -397,11 +605,21 @@ export default {
|
||||
},
|
||||
|
||||
pushURL() {
|
||||
return this.$root.baseURL + "/api/push/" + this.monitor.pushToken + "?status=up&msg=OK&ping=";
|
||||
return (
|
||||
this.$root.baseURL +
|
||||
"/api/push/" +
|
||||
this.monitor.pushToken +
|
||||
"?status=up&msg=OK&ping="
|
||||
);
|
||||
},
|
||||
|
||||
screenshotURL() {
|
||||
return getResBaseURL() + this.monitor.screenshot + "?time=" + this.cacheTime;
|
||||
return (
|
||||
getResBaseURL() +
|
||||
this.monitor.screenshot +
|
||||
"?time=" +
|
||||
this.cacheTime
|
||||
);
|
||||
},
|
||||
|
||||
descriptionHTML() {
|
||||
@@ -410,7 +628,7 @@ export default {
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
@@ -434,7 +652,10 @@ export default {
|
||||
mounted() {
|
||||
this.getImportantHeartbeatListLength();
|
||||
|
||||
this.$root.emitter.on("newImportantHeartbeat", this.onNewImportantHeartbeat);
|
||||
this.$root.emitter.on(
|
||||
"newImportantHeartbeat",
|
||||
this.onNewImportantHeartbeat
|
||||
);
|
||||
|
||||
if (this.monitor && this.monitor.type === "push") {
|
||||
if (this.lastHeartBeat.status === -1) {
|
||||
@@ -445,7 +666,10 @@ export default {
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
this.$root.emitter.off("newImportantHeartbeat", this.onNewImportantHeartbeat);
|
||||
this.$root.emitter.off(
|
||||
"newImportantHeartbeat",
|
||||
this.onNewImportantHeartbeat
|
||||
);
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -472,9 +696,11 @@ export default {
|
||||
* @returns {void}
|
||||
*/
|
||||
resumeMonitor() {
|
||||
this.$root.getSocket().emit("resumeMonitor", this.monitor.id, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
});
|
||||
this.$root
|
||||
.getSocket()
|
||||
.emit("resumeMonitor", this.monitor.id, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -482,9 +708,11 @@ export default {
|
||||
* @returns {void}
|
||||
*/
|
||||
pauseMonitor() {
|
||||
this.$root.getSocket().emit("pauseMonitor", this.monitor.id, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
});
|
||||
this.$root
|
||||
.getSocket()
|
||||
.emit("pauseMonitor", this.monitor.id, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -552,7 +780,7 @@ export default {
|
||||
*/
|
||||
clearHeartbeats() {
|
||||
this.$root.clearHeartbeats(this.monitor.id, (res) => {
|
||||
if (! res.ok) {
|
||||
if (!res.ok) {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
});
|
||||
@@ -569,7 +797,11 @@ export default {
|
||||
translationPrefix = "Avg. ";
|
||||
}
|
||||
|
||||
if (this.monitor.type === "http" || this.monitor.type === "keyword" || this.monitor.type === "json-query") {
|
||||
if (
|
||||
this.monitor.type === "http" ||
|
||||
this.monitor.type === "keyword" ||
|
||||
this.monitor.type === "json-query"
|
||||
) {
|
||||
return this.$t(translationPrefix + "Response");
|
||||
}
|
||||
|
||||
@@ -599,7 +831,10 @@ export default {
|
||||
return parsedUrl.toString();
|
||||
} catch (e) {
|
||||
// Handle SQL Server
|
||||
return urlString.replaceAll(/Password=(.+);/ig, "Password=******;");
|
||||
return urlString.replaceAll(
|
||||
/Password=(.+);/gi,
|
||||
"Password=******;"
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -609,12 +844,18 @@ export default {
|
||||
*/
|
||||
getImportantHeartbeatListLength() {
|
||||
if (this.monitor) {
|
||||
this.$root.getSocket().emit("monitorImportantHeartbeatListCount", this.monitor.id, (res) => {
|
||||
if (res.ok) {
|
||||
this.importantHeartBeatListLength = res.count;
|
||||
this.getImportantHeartbeatListPaged();
|
||||
}
|
||||
});
|
||||
this.$root
|
||||
.getSocket()
|
||||
.emit(
|
||||
"monitorImportantHeartbeatListCount",
|
||||
this.monitor.id,
|
||||
(res) => {
|
||||
if (res.ok) {
|
||||
this.importantHeartBeatListLength = res.count;
|
||||
this.getImportantHeartbeatListPaged();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -625,11 +866,19 @@ export default {
|
||||
getImportantHeartbeatListPaged() {
|
||||
if (this.monitor) {
|
||||
const offset = (this.page - 1) * this.perPage;
|
||||
this.$root.getSocket().emit("monitorImportantHeartbeatListPaged", this.monitor.id, offset, this.perPage, (res) => {
|
||||
if (res.ok) {
|
||||
this.displayedRecords = res.data;
|
||||
}
|
||||
});
|
||||
this.$root
|
||||
.getSocket()
|
||||
.emit(
|
||||
"monitorImportantHeartbeatListPaged",
|
||||
this.monitor.id,
|
||||
offset,
|
||||
this.perPage,
|
||||
(res) => {
|
||||
if (res.ok) {
|
||||
this.displayedRecords = res.data;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -661,13 +910,26 @@ export default {
|
||||
|
||||
loadPushExample() {
|
||||
this.pushMonitor.code = "";
|
||||
this.$root.getSocket().emit("getPushExample", this.pushMonitor.currentExample, (res) => {
|
||||
let code = res.code
|
||||
.replace("60", this.monitor.interval)
|
||||
.replace("https://example.com/api/push/key?status=up&msg=OK&ping=", this.pushURL);
|
||||
this.pushMonitor.code = code;
|
||||
});
|
||||
}
|
||||
this.$root
|
||||
.getSocket()
|
||||
.emit(
|
||||
"getPushExample",
|
||||
this.pushMonitor.currentExample,
|
||||
(res) => {
|
||||
let code = res.code
|
||||
.replace("60", this.monitor.interval)
|
||||
.replace(
|
||||
"https://example.com/api/push/key?status=up&msg=OK&ping=",
|
||||
this.pushURL
|
||||
);
|
||||
this.pushMonitor.code = code;
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
secondsToHumanReadableFormat(seconds) {
|
||||
return relativeTimeFormatter.secondsToHumanReadableFormat(seconds);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@@ -311,6 +311,13 @@
|
||||
required
|
||||
data-testid="hostname-input"
|
||||
>
|
||||
<div v-if="monitor.type === 'mqtt'" class="form-text">
|
||||
<i18n-t tag="p" keypath="mqttHostnameTip">
|
||||
<template #hostnameFormat>
|
||||
<code>[mqtt,ws,wss]://hostname</code>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Port -->
|
||||
@@ -483,6 +490,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="mqttWebsocketPath" class="form-label">{{ $t("mqttWebSocketPath") }}</label>
|
||||
<input
|
||||
v-if="/wss?:\/\/.+/.test(monitor.hostname)"
|
||||
id="mqttWebsocketPath"
|
||||
v-model="monitor.mqttWebsocketPath"
|
||||
type="text"
|
||||
class="form-control"
|
||||
>
|
||||
<input v-else type="text" class="form-control" disabled>
|
||||
<div class="form-text">
|
||||
{{ $t("mqttWebsocketPathExplanation") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="mqttCheckType" class="form-label">MQTT {{ $t("Check Type") }}</label>
|
||||
<select id="mqttCheckType" v-model="monitor.mqttCheckType" class="form-select" required>
|
||||
@@ -607,6 +629,9 @@
|
||||
<div class="my-3">
|
||||
<label for="interval" class="form-label">{{ $t("Heartbeat Interval") }} ({{ $t("checkEverySecond", [ monitor.interval ]) }})</label>
|
||||
<input id="interval" v-model="monitor.interval" type="number" class="form-control" required :min="minInterval" step="1" :max="maxInterval" @blur="finishUpdateInterval">
|
||||
<div class="form-text">
|
||||
{{ monitor.humanReadableInterval }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
@@ -1148,7 +1173,7 @@ import {
|
||||
MIN_INTERVAL_SECOND,
|
||||
sleep,
|
||||
} from "../util.ts";
|
||||
import { hostNameRegexPattern } from "../util-frontend";
|
||||
import { hostNameRegexPattern, relativeTimeFormatter } from "../util-frontend";
|
||||
import HiddenInput from "../components/HiddenInput.vue";
|
||||
import EditMonitorConditions from "../components/EditMonitorConditions.vue";
|
||||
|
||||
@@ -1164,6 +1189,7 @@ const monitorDefaults = {
|
||||
method: "GET",
|
||||
ipFamily: null,
|
||||
interval: 60,
|
||||
humanReadableInterval: relativeTimeFormatter.secondsToHumanReadableFormat(60),
|
||||
retryInterval: 60,
|
||||
resendInterval: 0,
|
||||
maxretries: 0,
|
||||
@@ -1181,6 +1207,7 @@ const monitorDefaults = {
|
||||
mqttUsername: "",
|
||||
mqttPassword: "",
|
||||
mqttTopic: "",
|
||||
mqttWebsocketPath: "",
|
||||
mqttSuccessMessage: "",
|
||||
mqttCheckType: "keyword",
|
||||
authMethod: null,
|
||||
@@ -1545,6 +1572,8 @@ message HealthCheckResponse {
|
||||
if (this.monitor.retryInterval === oldValue) {
|
||||
this.monitor.retryInterval = value;
|
||||
}
|
||||
// Converting monitor.interval to human readable format.
|
||||
this.monitor.humanReadableInterval = relativeTimeFormatter.secondsToHumanReadableFormat(value);
|
||||
},
|
||||
|
||||
"monitor.timeout"(value, oldValue) {
|
||||
@@ -1845,6 +1874,16 @@ message HealthCheckResponse {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate MQTT WebSocket Path pattern if present
|
||||
if (this.monitor.type === "mqtt" && this.monitor.mqttWebsocketPath) {
|
||||
const pattern = /^\/[A-Za-z0-9-_&()*+]*$/;
|
||||
if (!pattern.test(this.monitor.mqttWebsocketPath)) {
|
||||
toast.error(this.$t("mqttWebsocketPathInvalid"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
|
@@ -22,6 +22,12 @@
|
||||
<div class="title">{{ statusPage.title }}</div>
|
||||
<div class="slug">/status/{{ statusPage.slug }}</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn btn-danger delete-status-page" @click.stop.prevent="deleteDialog(statusPage.slug)">
|
||||
<font-awesome-icon icon="trash" />
|
||||
<span>{{ $t("Delete") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
<div v-else class="d-flex align-items-center justify-content-center my-3 spinner">
|
||||
@@ -30,18 +36,22 @@
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteStatusPage">
|
||||
{{ $t("deleteStatusPageMsg") }}
|
||||
</Confirm>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Confirm from "../components/Confirm.vue";
|
||||
import { getResBaseURL } from "../util-frontend";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
||||
Confirm
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedStatusSlug: ""
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -62,6 +72,20 @@ export default {
|
||||
} else {
|
||||
return getResBaseURL() + icon;
|
||||
}
|
||||
},
|
||||
deleteDialog(slug) {
|
||||
this.$data.selectedStatusSlug = slug;
|
||||
this.$refs.confirmDelete.show();
|
||||
},
|
||||
deleteStatusPage() {
|
||||
this.$root.getSocket().emit("deleteStatusPage", this.$data.selectedStatusSlug, (res) => {
|
||||
if (res.ok) {
|
||||
this.$root.toastSuccess(this.$t("successDeleted"));
|
||||
window.location.reload();
|
||||
} else {
|
||||
this.$root.toastError(res.msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -81,6 +105,10 @@ export default {
|
||||
|
||||
&:hover {
|
||||
background-color: $highlight-white;
|
||||
|
||||
& .actions {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
@@ -98,6 +126,8 @@ export default {
|
||||
}
|
||||
|
||||
.info {
|
||||
flex: 1 1 auto;
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
@@ -107,6 +137,19 @@ export default {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
visibility: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.delete-status-page {
|
||||
flex: 1 1 auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
@@ -120,4 +163,20 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 770px) {
|
||||
.item {
|
||||
.actions {
|
||||
visibility: visible;
|
||||
|
||||
.btn {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -720,7 +720,7 @@ export default {
|
||||
// Configure auto-refresh loop
|
||||
feedInterval = setInterval(() => {
|
||||
this.updateHeartbeatList();
|
||||
}, (this.config.autoRefreshInterval + 10) * 1000);
|
||||
}, Math.max(5, this.config.autoRefreshInterval) * 1000);
|
||||
|
||||
this.updateUpdateTimer();
|
||||
}).catch( function (error) {
|
||||
@@ -806,7 +806,15 @@ export default {
|
||||
clearInterval(this.updateCountdown);
|
||||
|
||||
this.updateCountdown = setInterval(() => {
|
||||
const countdown = dayjs.duration(this.lastUpdateTime.add(this.config.autoRefreshInterval, "seconds").add(10, "seconds").diff(dayjs()));
|
||||
// rounding here as otherwise we sometimes skip numbers in cases of time drift
|
||||
const countdown = dayjs.duration(
|
||||
Math.round(
|
||||
this.lastUpdateTime
|
||||
.add(Math.max(5, this.config.autoRefreshInterval), "seconds")
|
||||
.diff(dayjs())
|
||||
/ 1000
|
||||
), "seconds");
|
||||
|
||||
if (countdown.as("seconds") < 0) {
|
||||
clearInterval(this.updateCountdown);
|
||||
} else {
|
||||
|
@@ -213,3 +213,78 @@ export function getToastErrorTimeout() {
|
||||
|
||||
return errorTimeout;
|
||||
}
|
||||
|
||||
class RelativeTimeFormatter {
|
||||
/**
|
||||
* Default locale and options for Relative Time Formatter
|
||||
*/
|
||||
constructor() {
|
||||
this.options = { numeric: "auto" };
|
||||
this.instance = new Intl.RelativeTimeFormat(currentLocale(), this.options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to update the instance locale and options
|
||||
* @param {string} locale Localization identifier (e.g., "en", "ar-sy") to update the instance with.
|
||||
* @returns {void} No return value.
|
||||
*/
|
||||
updateLocale(locale) {
|
||||
this.instance = new Intl.RelativeTimeFormat(locale, this.options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to convert seconds into Human readable format
|
||||
* @param {number} seconds Receive value in seconds.
|
||||
* @returns {string} String converted to Days Mins Seconds Format
|
||||
*/
|
||||
secondsToHumanReadableFormat(seconds) {
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor(((seconds % 86400) % 3600) / 60);
|
||||
const secs = ((seconds % 86400) % 3600) % 60;
|
||||
const parts = [];
|
||||
/**
|
||||
* Build the formatted string from parts
|
||||
* 1. Get the relative time formatted parts from the instance.
|
||||
* 2. Filter out the relevant parts literal (unit of time) or integer (value).
|
||||
* 3. Map out the required values.
|
||||
* @param {number} value Receives value in seconds.
|
||||
* @param {string} unitOfTime Expected unit of time after conversion.
|
||||
* @returns {void}
|
||||
*/
|
||||
const toFormattedPart = (value, unitOfTime) => {
|
||||
const partsArray = this.instance.formatToParts(value, unitOfTime);
|
||||
const filteredParts = partsArray
|
||||
.filter(
|
||||
(part, index) =>
|
||||
(part.type === "literal" || part.type === "integer") &&
|
||||
index > 0
|
||||
)
|
||||
.map((part) => part.value);
|
||||
|
||||
const formattedString = filteredParts.join("").trim();
|
||||
parts.push(formattedString);
|
||||
};
|
||||
|
||||
if (days > 0) {
|
||||
toFormattedPart(days, "days");
|
||||
}
|
||||
if (hours > 0) {
|
||||
toFormattedPart(hours, "hour");
|
||||
}
|
||||
if (minutes > 0) {
|
||||
toFormattedPart(minutes, "minute");
|
||||
}
|
||||
if (secs > 0) {
|
||||
toFormattedPart(secs, "second");
|
||||
}
|
||||
|
||||
if (parts.length > 0) {
|
||||
return `${parts.join(" ")}`;
|
||||
}
|
||||
return this.instance.format(0, "second"); // Handle case for 0 seconds
|
||||
}
|
||||
}
|
||||
|
||||
export const relativeTimeFormatter = new RelativeTimeFormatter();
|
||||
|
||||
|
@@ -23,6 +23,7 @@ async function testMqtt(mqttSuccessMessage, mqttCheckType, receivedMessage) {
|
||||
port: connectionString.split(":")[2],
|
||||
mqttUsername: null,
|
||||
mqttPassword: null,
|
||||
mqttWebsocketPath: null, // for WebSocket connections
|
||||
interval: 20, // controls the timeout
|
||||
mqttSuccessMessage: mqttSuccessMessage, // for keywords
|
||||
expectedValue: mqttSuccessMessage, // for json-query
|
||||
|
@@ -121,8 +121,8 @@ test.describe("Status Page", () => {
|
||||
|
||||
await expect(page.getByTestId("update-countdown-text")).toContainText("00:");
|
||||
const updateCountdown = Number((await page.getByTestId("update-countdown-text").textContent()).match(/(\d+):(\d+)/)[2]);
|
||||
expect(updateCountdown).toBeGreaterThanOrEqual(refreshInterval); // cant be certain when the timer will start, so ensure it's within expected range
|
||||
expect(updateCountdown).toBeLessThanOrEqual(refreshInterval + 10);
|
||||
expect(updateCountdown).toBeGreaterThanOrEqual(refreshInterval - 10); // cant be certain when the timer will start, so ensure it's within expected range
|
||||
expect(updateCountdown).toBeLessThanOrEqual(refreshInterval);
|
||||
|
||||
await expect(page.locator("body")).toHaveClass(theme);
|
||||
|
||||
|
Reference in New Issue
Block a user