Merge remote-tracking branch 'origin/master' into ntfy-icon

# Conflicts:
#	server/notification-providers/ntfy.js
#	src/components/notifications/Ntfy.vue
#	src/languages/en.js
This commit is contained in:
Louis Lam 2022-10-31 17:10:20 +08:00
commit fd15e7c2dc
121 changed files with 4931 additions and 2523 deletions

View file

@ -27,7 +27,7 @@ The frontend code build into "dist" directory. The server (express.js) exposes t
## Can I create a pull request for Uptime Kuma? ## Can I create a pull request for Uptime Kuma?
Yes or no, it depends on what you will try to do. Since I don't want to waste your time, be sure to **create empty draft pull request or open an issue, so we can discuss first**. Especially for a large pull request or you don't know it will be merged or not. Yes or no, it depends on what you will try to do. Since I don't want to waste your time, be sure to **create an empty draft pull request or open an issue, so we can discuss first**. Especially for a large pull request or you don't know it will be merged or not.
Here are some references: Here are some references:
@ -48,7 +48,7 @@ Here are some references:
- UI/UX is not close to Uptime Kuma - UI/UX is not close to Uptime Kuma
- Existing logic is completely modified or deleted for no reason - Existing logic is completely modified or deleted for no reason
- A function that is completely out of scope - A function that is completely out of scope
- Unnesscary large code changes (Hard to review, casuse code conflicts to other pull requests) - Unnecessary large code changes (Hard to review, causes code conflicts to other pull requests)
I will mark your pull request in the [milestones](https://github.com/louislam/uptime-kuma/milestones), if I am plan to review and merge it. I will mark your pull request in the [milestones](https://github.com/louislam/uptime-kuma/milestones), if I am plan to review and merge it.
@ -183,7 +183,7 @@ By default, the Chromium window will be shown up during the test. Specifying `HE
## Dependencies ## Dependencies
Both frontend and backend share the same package.json. However, the frontend dependencies are eventually not be used in production environment, because it is usually also baked into dist files. So: Both frontend and backend share the same package.json. However, the frontend dependencies are eventually not used in the production environment, because it is usually also baked into dist files. So:
- Frontend dependencies = "devDependencies" - Frontend dependencies = "devDependencies"
- Examples: vue, chart.js - Examples: vue, chart.js

View file

@ -15,11 +15,10 @@ It is a self-hosted monitoring tool like "Uptime Robot".
Try it! Try it!
https://demo.uptime.kuma.pet - Tokyo Demo Server: https://demo.uptime.kuma.pet (Sponsored by [Uptime Kuma Sponsors](https://github.com/louislam/uptime-kuma#%EF%B8%8F-sponsors))
- Europe Demo Server: https://demo.uptime-kuma.karimi.dev:27000 (Provided by [@mhkarimi1383](https://github.com/mhkarimi1383))
It is a temporary live demo, all data will be deleted after 10 minutes. The server is located in Tokyo, so if you live far from there, it may affect your experience. I suggest that you should install and try it out for the best demo experience. It is a temporary live demo, all data will be deleted after 10 minutes. Use the one that is closer to you, but I suggest that you should install and try it out for the best demo experience.
VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollective.com/uptime-kuma)! Thank you so much!
## ⭐ Features ## ⭐ Features

28
config/cypress.config.js Normal file
View file

@ -0,0 +1,28 @@
const { defineConfig } = require("cypress");
module.exports = defineConfig({
projectId: "vyjuem",
e2e: {
experimentalStudio: true,
setupNodeEvents(on, config) {
},
fixturesFolder: "test/cypress/fixtures",
screenshotsFolder: "test/cypress/screenshots",
videosFolder: "test/cypress/videos",
downloadsFolder: "test/cypress/downloads",
supportFile: "test/cypress/support/e2e.js",
baseUrl: "http://localhost:3002",
defaultCommandTimeout: 10000,
pageLoadTimeout: 60000,
viewportWidth: 1920,
viewportHeight: 1080,
specPattern: [
"test/cypress/e2e/setup.cy.js",
"test/cypress/e2e/**/*.js"
],
},
env: {
baseUrl: "http://localhost:3002",
},
});

View file

@ -1,33 +0,0 @@
const PuppeteerEnvironment = require("jest-environment-puppeteer");
const util = require("util");
class DebugEnv extends PuppeteerEnvironment {
async handleTestEvent(event, state) {
const ignoredEvents = [
"setup",
"add_hook",
"start_describe_definition",
"add_test",
"finish_describe_definition",
"run_start",
"run_describe_start",
"test_start",
"hook_start",
"hook_success",
"test_fn_start",
"test_fn_success",
"test_done",
"run_describe_finish",
"run_finish",
"teardown",
"test_fn_failure",
];
if (!ignoredEvents.includes(event.name)) {
console.log(
new Date().toString() + ` Unhandled event [${event.name}] ` + util.inspect(event)
);
}
}
}
module.exports = DebugEnv;

View file

@ -1,5 +0,0 @@
module.exports = {
"rootDir": "..",
"testRegex": "./test/frontend.spec.js",
};

View file

@ -1,20 +0,0 @@
module.exports = {
"launch": {
"dumpio": true,
"slowMo": 500,
"headless": process.env.HEADLESS_TEST || false,
"userDataDir": "./data/test-chrome-profile",
args: [
"--disable-setuid-sandbox",
"--disable-gpu",
"--disable-dev-shm-usage",
"--no-default-browser-check",
"--no-experiments",
"--no-first-run",
"--no-pings",
"--no-sandbox",
"--no-zygote",
"--single-process",
],
}
};

View file

@ -1,12 +0,0 @@
module.exports = {
"verbose": true,
"preset": "jest-puppeteer",
"globals": {
"__DEV__": true
},
"testRegex": "./test/e2e.spec.js",
"testEnvironment": "./config/jest-debug-env.js",
"rootDir": "..",
"testTimeout": 30000,
};

View file

@ -1,15 +0,0 @@
import { defineConfig } from "cypress";
export default defineConfig({
e2e: {
baseUrl: "http://localhost:3002",
defaultCommandTimeout: 10000,
pageLoadTimeout: 60000,
viewportWidth: 1920,
viewportHeight: 1080,
specPattern: ["cypress/e2e/setup.cy.ts", "cypress/e2e/**/*.ts"],
},
env: {
baseUrl: "http://localhost:3002",
},
});

View file

@ -1,24 +0,0 @@
import { actor } from "../support/actors/actor";
import { DEFAULT_USER_DATA } from "../support/const/user-data";
import { DashboardPage } from "../support/pages/dasboard-page";
import { SetupPage } from "../support/pages/setup-page";
describe("user can create a new account on setup page", () => {
before(() => {
cy.visit("/setup");
});
it("user can create new account", () => {
cy.url().should("be.equal", SetupPage.url);
actor.setupTask.fillAndSubmitSetupForm(
DEFAULT_USER_DATA.username,
DEFAULT_USER_DATA.password,
DEFAULT_USER_DATA.password
);
cy.url().should("be.equal", DashboardPage.url);
cy.get('[role="alert"]')
.should("be.visible")
.and("contain.text", "Added Successfully.");
});
});

View file

@ -1,8 +0,0 @@
import { SetupTask } from "../tasks/setup-task";
class Actor {
setupTask: SetupTask = new SetupTask();
}
const actor = new Actor();
export { actor };

View file

@ -1 +0,0 @@
import "./commands";

View file

@ -1,15 +0,0 @@
import { SetupPage } from "../pages/setup-page";
export class SetupTask {
fillAndSubmitSetupForm(
username: string,
password: string,
passwordRepeat: string
) {
cy.get(SetupPage.usernameInput).type(username);
cy.get(SetupPage.passWordInput).type(password);
cy.get(SetupPage.passwordRepeatInput).type(passwordRepeat);
cy.get(SetupPage.submitSetupForm).click();
}
}

View file

@ -0,0 +1,83 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
-- Just for someone who tested maintenance before (patch-maintenance-table.sql)
DROP TABLE IF EXISTS maintenance_status_page;
DROP TABLE IF EXISTS monitor_maintenance;
DROP TABLE IF EXISTS maintenance;
DROP TABLE IF EXISTS maintenance_timeslot;
-- maintenance
CREATE TABLE [maintenance] (
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
[title] VARCHAR(150) NOT NULL,
[description] TEXT NOT NULL,
[user_id] INTEGER REFERENCES [user]([id]) ON DELETE SET NULL ON UPDATE CASCADE,
[active] BOOLEAN NOT NULL DEFAULT 1,
[strategy] VARCHAR(50) NOT NULL DEFAULT 'single',
[start_date] DATETIME,
[end_date] DATETIME,
[start_time] TIME,
[end_time] TIME,
[weekdays] VARCHAR2(250) DEFAULT '[]',
[days_of_month] TEXT DEFAULT '[]',
[interval_day] INTEGER
);
CREATE INDEX [manual_active] ON [maintenance] (
[strategy],
[active]
);
CREATE INDEX [active] ON [maintenance] ([active]);
CREATE INDEX [maintenance_user_id] ON [maintenance] ([user_id]);
-- maintenance_status_page
CREATE TABLE maintenance_status_page (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
status_page_id INTEGER NOT NULL,
maintenance_id INTEGER NOT NULL,
CONSTRAINT FK_maintenance FOREIGN KEY (maintenance_id) REFERENCES maintenance (id) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT FK_status_page FOREIGN KEY (status_page_id) REFERENCES status_page (id) ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE INDEX [status_page_id_index]
ON [maintenance_status_page]([status_page_id]);
CREATE INDEX [maintenance_id_index]
ON [maintenance_status_page]([maintenance_id]);
-- maintenance_timeslot
CREATE TABLE [maintenance_timeslot] (
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
[maintenance_id] INTEGER NOT NULL CONSTRAINT [FK_maintenance] REFERENCES [maintenance]([id]) ON DELETE CASCADE ON UPDATE CASCADE,
[start_date] DATETIME NOT NULL,
[end_date] DATETIME,
[generated_next] BOOLEAN DEFAULT 0
);
CREATE INDEX [maintenance_id] ON [maintenance_timeslot] ([maintenance_id] DESC);
CREATE INDEX [active_timeslot_index] ON [maintenance_timeslot] (
[maintenance_id] DESC,
[start_date] DESC,
[end_date] DESC
);
CREATE INDEX [generated_next_index] ON [maintenance_timeslot] ([generated_next]);
-- monitor_maintenance
CREATE TABLE monitor_maintenance (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
monitor_id INTEGER NOT NULL,
maintenance_id INTEGER NOT NULL,
CONSTRAINT FK_maintenance FOREIGN KEY (maintenance_id) REFERENCES maintenance (id) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT FK_monitor FOREIGN KEY (monitor_id) REFERENCES monitor (id) ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE INDEX [maintenance_id_index2] ON [monitor_maintenance]([maintenance_id]);
CREATE INDEX [monitor_id_index] ON [monitor_maintenance]([monitor_id]);
COMMIT;

View file

@ -1,51 +1,45 @@
// Need to use ES6 to read language files // Need to use ES6 to read language files
import fs from "fs"; import fs from "fs";
import path from "path";
import util from "util"; import util from "util";
import rmSync from "../fs-rmSync.js"; import rmSync from "../fs-rmSync.js";
// https://stackoverflow.com/questions/13786160/copy-folder-recursively-in-node-js
/** /**
* Look ma, it's cp -R. * Copy across the required language files
* @param {string} src The path to the thing to copy. * Creates a local directory (./languages) and copies the required files
* @param {string} dest The path to the new copy. * into it.
* @param {string} langCode Code of language to update. A file will be
* created with this code if one does not already exist
* @param {string} baseLang The second base language file to copy. This
* will be ignored if set to "en" as en.js is copied by default
*/ */
const copyRecursiveSync = function (src, dest) { function copyFiles(langCode, baseLang) {
let exists = fs.existsSync(src); if (fs.existsSync("./languages")) {
let stats = exists && fs.statSync(src);
let isDirectory = exists && stats.isDirectory();
if (isDirectory) {
fs.mkdirSync(dest);
fs.readdirSync(src).forEach(function (childItemName) {
copyRecursiveSync(path.join(src, childItemName),
path.join(dest, childItemName));
});
} else {
fs.copyFileSync(src, dest);
}
};
console.log("Arguments:", process.argv);
const baseLangCode = process.argv[2] || "en";
console.log("Base Lang: " + baseLangCode);
if (fs.existsSync("./languages")) {
rmSync("./languages", { recursive: true }); rmSync("./languages", { recursive: true });
}
copyRecursiveSync("../../src/languages", "./languages");
const en = (await import("./languages/en.js")).default;
const baseLang = (await import(`./languages/${baseLangCode}.js`)).default;
const files = fs.readdirSync("./languages");
console.log("Files:", files);
for (const file of files) {
if (! file.endsWith(".js")) {
console.log("Skipping " + file);
continue;
} }
fs.mkdirSync("./languages");
if (!fs.existsSync(`../../src/languages/${langCode}.js`)) {
fs.closeSync(fs.openSync(`./languages/${langCode}.js`, "a"));
} else {
fs.copyFileSync(`../../src/languages/${langCode}.js`, `./languages/${langCode}.js`);
}
fs.copyFileSync("../../src/languages/en.js", "./languages/en.js");
if (baseLang !== "en") {
fs.copyFileSync(`../../src/languages/${baseLang}.js`, `./languages/${baseLang}.js`);
}
}
/**
* Update the specified language file
* @param {string} langCode Language code to update
* @param {string} baseLang Second language to copy keys from
*/
async function updateLanguage(langCode, baseLangCode) {
const en = (await import("./languages/en.js")).default;
const baseLang = (await import(`./languages/${baseLangCode}.js`)).default;
let file = langCode + ".js";
console.log("Processing " + file); console.log("Processing " + file);
const lang = await import("./languages/" + file); const lang = await import("./languages/" + file);
@ -83,5 +77,20 @@ for (const file of files) {
fs.writeFileSync(`../../src/languages/${file}`, code); fs.writeFileSync(`../../src/languages/${file}`, code);
} }
// Get command line arguments
const baseLangCode = process.env.npm_config_baselang || "en";
const langCode = process.env.npm_config_language;
// We need the file to edit
if (langCode == null) {
throw new Error("Argument --language=<code> must be provided");
}
console.log("Base Lang: " + baseLangCode);
console.log("Updating: " + langCode);
copyFiles(langCode, baseLangCode);
await updateLanguage(langCode, baseLangCode);
rmSync("./languages", { recursive: true }); rmSync("./languages", { recursive: true });
console.log("Done. Fixing formatting by ESLint..."); console.log("Done. Fixing formatting by ESLint...");

1906
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "uptime-kuma", "name": "uptime-kuma",
"version": "1.18.0", "version": "1.19.0-beta.0",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@ -22,12 +22,11 @@
"start": "npm run start-server", "start": "npm run start-server",
"start-server": "node server/server.js", "start-server": "node server/server.js",
"start-server-dev": "cross-env NODE_ENV=development node server/server.js", "start-server-dev": "cross-env NODE_ENV=development node server/server.js",
"start-server-watch-dev": "cross-env NODE_ENV=development node --watch server/server.js",
"build": "vite build --config ./config/vite.config.js", "build": "vite build --config ./config/vite.config.js",
"test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --test", "test": "node test/prepare-test-server.js && npm run jest-backend",
"test-with-build": "npm run build && npm test", "test-with-build": "npm run build && npm test",
"jest": "node test/prepare-jest.js && npm run jest-frontend && npm run jest-backend", "jest-backend": "cross-env TEST_BACKEND=1 jest --runInBand --detectOpenHandles --forceExit --config=./config/jest-backend.config.js",
"jest-frontend": "cross-env TEST_FRONTEND=1 jest --config=./config/jest-frontend.config.js",
"jest-backend": "cross-env TEST_BACKEND=1 jest --config=./config/jest-backend.config.js",
"tsc": "tsc", "tsc": "tsc",
"vite-preview-dist": "vite preview --host --config ./config/vite.config.js", "vite-preview-dist": "vite preview --host --config ./config/vite.config.js",
"build-docker": "npm run build && npm run build-docker-debian && npm run build-docker-alpine", "build-docker": "npm run build && npm run build-docker-debian && npm run build-docker-alpine",
@ -40,7 +39,7 @@
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain", "build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push", "build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push",
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain", "upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
"setup": "git checkout 1.18.0 && npm ci --production && npm run download-dist", "setup": "git checkout 1.18.5 && npm ci --production && npm run download-dist",
"download-dist": "node extra/download-dist.js", "download-dist": "node extra/download-dist.js",
"mark-as-nightly": "node extra/mark-as-nightly.js", "mark-as-nightly": "node extra/mark-as-nightly.js",
"reset-password": "node extra/reset-password.js", "reset-password": "node extra/reset-password.js",
@ -53,8 +52,7 @@
"test-nodejs16": "docker build --progress plain -f test/ubuntu-nodejs16.dockerfile .", "test-nodejs16": "docker build --progress plain -f test/ubuntu-nodejs16.dockerfile .",
"simple-dns-server": "node extra/simple-dns-server.js", "simple-dns-server": "node extra/simple-dns-server.js",
"simple-mqtt-server": "node extra/simple-mqtt-server.js", "simple-mqtt-server": "node extra/simple-mqtt-server.js",
"update-language-files-with-base-lang": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix", "update-language-files": "cd extra/update-language-files && node index.js && cross-env-shell eslint ../../src/languages/$npm_config_language.js --fix",
"update-language-files": "cd extra/update-language-files && node index.js && eslint ../../src/languages/**.js --fix",
"ncu-patch": "npm-check-updates -u -t patch", "ncu-patch": "npm-check-updates -u -t patch",
"release-final": "node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js", "release-final": "node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js",
"release-beta": "node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta . --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts", "release-beta": "node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta . --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts",
@ -62,52 +60,54 @@
"build-dist-and-restart": "npm run build && npm run start-server-dev", "build-dist-and-restart": "npm run build && npm run start-server-dev",
"start-pr-test": "node extra/checkout-pr.js && npm install && npm run dev", "start-pr-test": "node extra/checkout-pr.js && npm install && npm run dev",
"cy:test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --e2e", "cy:test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --e2e",
"cy:run": "npx cypress run --browser chrome --headless" "cy:run": "npx cypress run --browser chrome --headless --config-file ./config/cypress.config.js",
"cypress-open": "concurrently -k -r \"node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/\" \"cypress open --config-file ./config/cypress.config.js\""
}, },
"dependencies": { "dependencies": {
"@louislam/sqlite3": "~15.0.6", "@louislam/sqlite3": "15.1.2",
"args-parser": "~1.3.0", "args-parser": "~1.3.0",
"axios": "~0.27.0", "axios": "~0.27.0",
"axios-ntlm": "^1.3.0", "axios-ntlm": "~1.3.0",
"badge-maker": "^3.3.1", "badge-maker": "~3.3.1",
"bcryptjs": "~2.4.3", "bcryptjs": "~2.4.3",
"bree": "~7.1.5", "bree": "~7.1.5",
"cacheable-lookup": "~6.0.4", "cacheable-lookup": "~6.0.4",
"chardet": "^1.3.0", "chardet": "~1.4.0",
"check-password-strength": "^2.0.5", "check-password-strength": "^2.0.5",
"cheerio": "^1.0.0-rc.10", "cheerio": "~1.0.0-rc.12",
"chroma-js": "^2.1.2", "chroma-js": "~2.4.2",
"command-exists": "~1.2.9", "command-exists": "~1.2.9",
"compare-versions": "~3.6.0", "compare-versions": "~3.6.0",
"compression": "^1.7.4", "compression": "~1.7.4",
"dayjs": "^1.11.0", "dayjs": "~1.11.5",
"express": "~4.17.3", "express": "~4.17.3",
"express-basic-auth": "~1.2.1", "express-basic-auth": "~1.2.1",
"express-static-gzip": "^2.1.7", "express-static-gzip": "~2.1.7",
"form-data": "~4.0.0", "form-data": "~4.0.0",
"http-graceful-shutdown": "~3.1.7", "http-graceful-shutdown": "~3.1.7",
"http-proxy-agent": "^5.0.0", "http-proxy-agent": "~5.0.0",
"https-proxy-agent": "^5.0.0", "https-proxy-agent": "~5.0.1",
"iconv-lite": "^0.6.3", "iconv-lite": "~0.6.3",
"jsesc": "~3.0.2",
"jsonwebtoken": "~8.5.1", "jsonwebtoken": "~8.5.1",
"jwt-decode": "^3.1.2", "jwt-decode": "~3.1.2",
"limiter": "^2.1.0", "limiter": "~2.1.0",
"mqtt": "^4.2.8", "mqtt": "~4.3.7",
"mssql": "^8.1.0", "mssql": "~8.1.4",
"node-cloudflared-tunnel": "~1.0.9", "node-cloudflared-tunnel": "~1.0.9",
"node-radius-client": "^1.0.0", "node-radius-client": "~1.0.0",
"nodemailer": "~6.6.5", "nodemailer": "~6.6.5",
"notp": "~2.0.3", "notp": "~2.0.3",
"password-hash": "~1.2.2", "password-hash": "~1.2.2",
"pg": "^8.7.3", "pg": "~8.8.0",
"pg-connection-string": "^2.5.0", "pg-connection-string": "~2.5.0",
"prom-client": "~13.2.0", "prom-client": "~13.2.0",
"prometheus-api-metrics": "~3.2.1", "prometheus-api-metrics": "~3.2.1",
"redbean-node": "0.1.4", "redbean-node": "0.1.4",
"socket.io": "~4.4.1", "socket.io": "~4.5.3",
"socket.io-client": "~4.4.1", "socket.io-client": "~4.5.3",
"socks-proxy-agent": "6.1.1", "socks-proxy-agent": "6.1.1",
"tar": "^6.1.11", "tar": "~6.1.11",
"tcp-ping": "~0.1.1", "tcp-ping": "~0.1.1",
"thirty-two": "~1.0.2" "thirty-two": "~1.0.2"
}, },
@ -124,6 +124,7 @@
"@vitejs/plugin-legacy": "~2.1.0", "@vitejs/plugin-legacy": "~2.1.0",
"@vitejs/plugin-vue": "~3.1.0", "@vitejs/plugin-vue": "~3.1.0",
"@vue/compiler-sfc": "~3.2.36", "@vue/compiler-sfc": "~3.2.36",
"@vuepic/vue-datepicker": "~3.4.8",
"aedes": "^0.46.3", "aedes": "^0.46.3",
"babel-plugin-rewire": "~1.2.0", "babel-plugin-rewire": "~1.2.0",
"bootstrap": "5.1.3", "bootstrap": "5.1.3",
@ -137,20 +138,18 @@
"dns2": "~2.0.1", "dns2": "~2.0.1",
"eslint": "~8.14.0", "eslint": "~8.14.0",
"eslint-plugin-vue": "~8.7.1", "eslint-plugin-vue": "~8.7.1",
"favico.js": "^0.3.10", "favico.js": "~0.3.10",
"jest": "~27.2.5", "jest": "~27.2.5",
"jest-puppeteer": "~6.0.3",
"postcss-html": "~1.5.0", "postcss-html": "~1.5.0",
"postcss-rtlcss": "~3.7.2", "postcss-rtlcss": "~3.7.2",
"postcss-scss": "~4.0.4", "postcss-scss": "~4.0.4",
"prismjs": "^1.27.0", "prismjs": "~1.29.0",
"puppeteer": "~13.1.3",
"qrcode": "~1.5.0", "qrcode": "~1.5.0",
"rollup-plugin-visualizer": "^5.6.0", "rollup-plugin-visualizer": "^5.6.0",
"sass": "~1.42.1", "sass": "~1.42.1",
"stylelint": "~14.7.1", "stylelint": "~14.7.1",
"stylelint-config-standard": "~25.0.0", "stylelint-config-standard": "~25.0.0",
"terser": "^5.15.0", "terser": "~5.15.0",
"timezones-list": "~3.0.1", "timezones-list": "~3.0.1",
"typescript": "~4.4.4", "typescript": "~4.4.4",
"v-pagination-3": "~0.1.7", "v-pagination-3": "~0.1.7",
@ -160,10 +159,10 @@
"vue-chart-3": "3.0.9", "vue-chart-3": "3.0.9",
"vue-confirm-dialog": "~1.0.2", "vue-confirm-dialog": "~1.0.2",
"vue-contenteditable": "~3.0.4", "vue-contenteditable": "~3.0.4",
"vue-i18n": "~9.1.9", "vue-i18n": "~9.2.2",
"vue-image-crop-upload": "~3.0.3", "vue-image-crop-upload": "~3.0.3",
"vue-multiselect": "~3.0.0-alpha.2", "vue-multiselect": "~3.0.0-alpha.2",
"vue-prism-editor": "^2.0.0-alpha.2", "vue-prism-editor": "~2.0.0-alpha.2",
"vue-qrcode": "~1.0.0", "vue-qrcode": "~1.0.0",
"vue-router": "~4.0.14", "vue-router": "~4.0.14",
"vue-toastification": "~2.0.0-rc.5", "vue-toastification": "~2.0.0-rc.5",

View file

@ -4,7 +4,8 @@
const { TimeLogger } = require("../src/util"); const { TimeLogger } = require("../src/util");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { UptimeKumaServer } = require("./uptime-kuma-server"); const { UptimeKumaServer } = require("./uptime-kuma-server");
const io = UptimeKumaServer.getInstance().io; const server = UptimeKumaServer.getInstance();
const io = server.io;
const { setting } = require("./util-server"); const { setting } = require("./util-server");
const checkVersion = require("./check-version"); const checkVersion = require("./check-version");
@ -121,7 +122,9 @@ async function sendInfo(socket) {
socket.emit("info", { socket.emit("info", {
version: checkVersion.version, version: checkVersion.version,
latestVersion: checkVersion.latestVersion, latestVersion: checkVersion.latestVersion,
primaryBaseURL: await setting("primaryBaseURL") primaryBaseURL: await setting("primaryBaseURL"),
serverTimezone: await server.getTimezone(),
serverTimezoneOffset: server.getTimezoneOffset(),
}); });
} }

View file

@ -64,6 +64,7 @@ class Database {
"patch-add-other-auth.sql": { parents: [ "patch-monitor-basic-auth.sql" ] }, "patch-add-other-auth.sql": { parents: [ "patch-monitor-basic-auth.sql" ] },
"patch-add-radius-monitor.sql": true, "patch-add-radius-monitor.sql": true,
"patch-monitor-add-resend-interval.sql": true, "patch-monitor-add-resend-interval.sql": true,
"patch-maintenance-table2.sql": true,
}; };
/** /**

View file

@ -75,7 +75,7 @@ class DockerHost {
if (dockerHost.dockerType === "socket") { if (dockerHost.dockerType === "socket") {
options.socketPath = dockerHost.dockerDaemon; options.socketPath = dockerHost.dockerDaemon;
} else if (dockerHost.dockerType === "tcp") { } else if (dockerHost.dockerType === "tcp") {
options.baseURL = dockerHost.dockerDaemon; options.baseURL = DockerHost.patchDockerURL(dockerHost.dockerDaemon);
} }
let res = await axios.request(options); let res = await axios.request(options);
@ -99,6 +99,18 @@ class DockerHost {
} }
} }
/**
* Since axios 0.27.X, it does not accept `tcp://` protocol.
* Change it to `http://` on the fly in order to fix it. (https://github.com/louislam/uptime-kuma/issues/2165)
*/
static patchDockerURL(url) {
if (typeof url === "string") {
// Replace the first occurrence only with g
return url.replace(/tcp:\/\//g, "http://");
}
return url;
}
} }
module.exports = { module.exports = {

View file

@ -1,8 +1,3 @@
const dayjs = require("dayjs");
const utc = require("dayjs/plugin/utc");
let timezone = require("dayjs/plugin/timezone");
dayjs.extend(utc);
dayjs.extend(timezone);
const { BeanModel } = require("redbean-node/dist/bean-model"); const { BeanModel } = require("redbean-node/dist/bean-model");
/** /**
@ -10,6 +5,7 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
* 0 = DOWN * 0 = DOWN
* 1 = UP * 1 = UP
* 2 = PENDING * 2 = PENDING
* 3 = MAINTENANCE
*/ */
class Heartbeat extends BeanModel { class Heartbeat extends BeanModel {

215
server/model/maintenance.js Normal file
View file

@ -0,0 +1,215 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
const { parseTimeObject, parseTimeFromTimeObject, utcToLocal, localToUTC, log } = require("../../src/util");
const { timeObjectToUTC, timeObjectToLocal } = require("../util-server");
const { R } = require("redbean-node");
const dayjs = require("dayjs");
class Maintenance extends BeanModel {
/**
* Return an object that ready to parse to JSON for public
* Only show necessary data to public
* @returns {Object}
*/
async toPublicJSON() {
let dateRange = [];
if (this.start_date) {
dateRange.push(utcToLocal(this.start_date));
if (this.end_date) {
dateRange.push(utcToLocal(this.end_date));
}
}
let timeRange = [];
let startTime = timeObjectToLocal(parseTimeObject(this.start_time));
timeRange.push(startTime);
let endTime = timeObjectToLocal(parseTimeObject(this.end_time));
timeRange.push(endTime);
let obj = {
id: this.id,
title: this.title,
description: this.description,
strategy: this.strategy,
intervalDay: this.interval_day,
active: !!this.active,
dateRange: dateRange,
timeRange: timeRange,
weekdays: (this.weekdays) ? JSON.parse(this.weekdays) : [],
daysOfMonth: (this.days_of_month) ? JSON.parse(this.days_of_month) : [],
timeslotList: [],
};
const timeslotList = await this.getTimeslotList();
for (let timeslot of timeslotList) {
obj.timeslotList.push(await timeslot.toPublicJSON());
}
if (!Array.isArray(obj.weekdays)) {
obj.weekdays = [];
}
if (!Array.isArray(obj.daysOfMonth)) {
obj.daysOfMonth = [];
}
// Maintenance Status
if (!obj.active) {
obj.status = "inactive";
} else if (obj.strategy === "manual") {
obj.status = "under-maintenance";
} else if (obj.timeslotList.length > 0) {
let currentTimestamp = dayjs().unix();
for (let timeslot of obj.timeslotList) {
if (dayjs.utc(timeslot.startDate).unix() <= currentTimestamp && dayjs.utc(timeslot.endDate).unix() >= currentTimestamp) {
log.debug("timeslot", "Timeslot ID: " + timeslot.id);
log.debug("timeslot", "currentTimestamp:" + currentTimestamp);
log.debug("timeslot", "timeslot.start_date:" + dayjs.utc(timeslot.startDate).unix());
log.debug("timeslot", "timeslot.end_date:" + dayjs.utc(timeslot.endDate).unix());
obj.status = "under-maintenance";
break;
}
}
if (!obj.status) {
obj.status = "scheduled";
}
} else if (obj.timeslotList.length === 0) {
obj.status = "ended";
} else {
obj.status = "unknown";
}
return obj;
}
/**
* Only get future or current timeslots only
* @returns {Promise<[]>}
*/
async getTimeslotList() {
return R.convertToBeans("maintenance_timeslot", await R.getAll(`
SELECT maintenance_timeslot.*
FROM maintenance_timeslot, maintenance
WHERE maintenance_timeslot.maintenance_id = maintenance.id
AND maintenance.id = ?
AND ${Maintenance.getActiveAndFutureMaintenanceSQLCondition()}
`, [
this.id
]));
}
/**
* Return an object that ready to parse to JSON
* @param {string} timezone If not specified, the timeRange will be in UTC
* @returns {Object}
*/
async toJSON(timezone = null) {
return this.toPublicJSON(timezone);
}
getDayOfWeekList() {
log.debug("timeslot", "List: " + this.weekdays);
return JSON.parse(this.weekdays).sort(function (a, b) {
return a - b;
});
}
getDayOfMonthList() {
return JSON.parse(this.days_of_month).sort(function (a, b) {
return a - b;
});
}
getStartDateTime() {
let startOfTheDay = dayjs.utc(this.start_date).format("HH:mm");
log.debug("timeslot", "startOfTheDay: " + startOfTheDay);
// Start Time
let startTimeSecond = dayjs.utc(this.start_time, "HH:mm").diff(dayjs.utc(startOfTheDay, "HH:mm"), "second");
log.debug("timeslot", "startTime: " + startTimeSecond);
// Bake StartDate + StartTime = Start DateTime
return dayjs.utc(this.start_date).add(startTimeSecond, "second");
}
getDuration() {
let duration = dayjs.utc(this.end_time, "HH:mm").diff(dayjs.utc(this.start_time, "HH:mm"), "second");
// Add 24hours if it is across day
if (duration < 0) {
duration += 24 * 3600;
}
return duration;
}
static jsonToBean(bean, obj) {
if (obj.id) {
bean.id = obj.id;
}
// Apply timezone offset to timeRange, as it cannot apply automatically.
if (obj.timeRange[0]) {
timeObjectToUTC(obj.timeRange[0]);
if (obj.timeRange[1]) {
timeObjectToUTC(obj.timeRange[1]);
}
}
bean.title = obj.title;
bean.description = obj.description;
bean.strategy = obj.strategy;
bean.interval_day = obj.intervalDay;
bean.active = obj.active;
if (obj.dateRange[0]) {
bean.start_date = localToUTC(obj.dateRange[0]);
if (obj.dateRange[1]) {
bean.end_date = localToUTC(obj.dateRange[1]);
}
}
bean.start_time = parseTimeFromTimeObject(obj.timeRange[0]);
bean.end_time = parseTimeFromTimeObject(obj.timeRange[1]);
bean.weekdays = JSON.stringify(obj.weekdays);
bean.days_of_month = JSON.stringify(obj.daysOfMonth);
return bean;
}
/**
* SQL conditions for active maintenance
* @returns {string}
*/
static getActiveMaintenanceSQLCondition() {
return `
(maintenance_timeslot.start_date <= DATETIME('now')
AND maintenance_timeslot.end_date >= DATETIME('now')
AND maintenance.active = 1)
OR
(maintenance.strategy = 'manual' AND active = 1)
`;
}
/**
* SQL conditions for active and future maintenance
* @returns {string}
*/
static getActiveAndFutureMaintenanceSQLCondition() {
return `
((maintenance_timeslot.end_date >= DATETIME('now')
AND maintenance.active = 1)
OR
(maintenance.strategy = 'manual' AND active = 1))
`;
}
}
module.exports = Maintenance;

View file

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

View file

@ -1,12 +1,8 @@
const https = require("https"); const https = require("https");
const dayjs = require("dayjs"); const dayjs = require("dayjs");
const utc = require("dayjs/plugin/utc");
let timezone = require("dayjs/plugin/timezone");
dayjs.extend(utc);
dayjs.extend(timezone);
const axios = require("axios"); const axios = require("axios");
const { Prometheus } = require("../prometheus"); const { Prometheus } = require("../prometheus");
const { log, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util"); const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger } = require("../../src/util");
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mqttAsync, setSetting, httpNtlm, radius } = require("../util-server"); const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mqttAsync, setSetting, httpNtlm, radius } = require("../util-server");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model"); const { BeanModel } = require("redbean-node/dist/bean-model");
@ -17,12 +13,15 @@ const version = require("../../package.json").version;
const apicache = require("../modules/apicache"); const apicache = require("../modules/apicache");
const { UptimeKumaServer } = require("../uptime-kuma-server"); const { UptimeKumaServer } = require("../uptime-kuma-server");
const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent"); const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
const { DockerHost } = require("../docker");
const Maintenance = require("./maintenance");
/** /**
* status: * status:
* 0 = DOWN * 0 = DOWN
* 1 = UP * 1 = UP
* 2 = PENDING * 2 = PENDING
* 3 = MAINTENANCE
*/ */
class Monitor extends BeanModel { class Monitor extends BeanModel {
@ -36,6 +35,7 @@ class Monitor extends BeanModel {
id: this.id, id: this.id,
name: this.name, name: this.name,
sendUrl: this.sendUrl, sendUrl: this.sendUrl,
maintenance: await Monitor.isUnderMaintenance(this.id),
}; };
if (this.sendUrl) { if (this.sendUrl) {
@ -95,6 +95,7 @@ class Monitor extends BeanModel {
proxyId: this.proxy_id, proxyId: this.proxy_id,
notificationIDList, notificationIDList,
tags: tags, tags: tags,
maintenance: await Monitor.isUnderMaintenance(this.id),
mqttUsername: this.mqttUsername, mqttUsername: this.mqttUsername,
mqttPassword: this.mqttPassword, mqttPassword: this.mqttPassword,
mqttTopic: this.mqttTopic, mqttTopic: this.mqttTopic,
@ -229,7 +230,10 @@ class Monitor extends BeanModel {
} }
try { try {
if (this.type === "http" || this.type === "keyword") { if (await Monitor.isUnderMaintenance(this.id)) {
bean.msg = "Monitor under maintenance";
bean.status = MAINTENANCE;
} else if (this.type === "http" || this.type === "keyword") {
// Do not do any queries/high loading things before the "bean.ping" // Do not do any queries/high loading things before the "bean.ping"
let startTime = dayjs().valueOf(); let startTime = dayjs().valueOf();
@ -498,7 +502,7 @@ class Monitor extends BeanModel {
if (dockerHost._dockerType === "socket") { if (dockerHost._dockerType === "socket") {
options.socketPath = dockerHost._dockerDaemon; options.socketPath = dockerHost._dockerDaemon;
} else if (dockerHost._dockerType === "tcp") { } else if (dockerHost._dockerType === "tcp") {
options.baseURL = dockerHost._dockerDaemon; options.baseURL = DockerHost.patchDockerURL(dockerHost._dockerDaemon);
} }
log.debug(`[${this.name}] Axios Request`); log.debug(`[${this.name}] Axios Request`);
@ -533,6 +537,17 @@ class Monitor extends BeanModel {
bean.ping = dayjs().valueOf() - startTime; bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "radius") { } else if (this.type === "radius") {
let startTime = dayjs().valueOf(); let startTime = dayjs().valueOf();
// Handle monitors that were created before the
// update and as such don't have a value for
// this.port.
let port;
if (this.port == null) {
port = 1812;
} else {
port = this.port;
}
try { try {
const resp = await radius( const resp = await radius(
this.hostname, this.hostname,
@ -540,7 +555,8 @@ class Monitor extends BeanModel {
this.radiusPassword, this.radiusPassword,
this.radiusCalledStationId, this.radiusCalledStationId,
this.radiusCallingStationId, this.radiusCallingStationId,
this.radiusSecret this.radiusSecret,
port
); );
if (resp.code) { if (resp.code) {
bean.msg = resp.code; bean.msg = resp.code;
@ -593,8 +609,12 @@ class Monitor extends BeanModel {
if (isImportant) { if (isImportant) {
bean.important = true; bean.important = true;
if (Monitor.isImportantForNotification(isFirstBeat, previousBeat?.status, bean.status)) {
log.debug("monitor", `[${this.name}] sendNotification`); log.debug("monitor", `[${this.name}] sendNotification`);
await Monitor.sendNotification(isFirstBeat, this, bean); await Monitor.sendNotification(isFirstBeat, this, bean);
} else {
log.debug("monitor", `[${this.name}] will not sendNotification because it is (or was) under maintenance`);
}
// Reset down count // Reset down count
bean.downCount = 0; bean.downCount = 0;
@ -603,6 +623,8 @@ class Monitor extends BeanModel {
log.debug("monitor", `[${this.name}] apicache clear`); log.debug("monitor", `[${this.name}] apicache clear`);
apicache.clear(); apicache.clear();
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
} else { } else {
bean.important = false; bean.important = false;
@ -626,6 +648,8 @@ class Monitor extends BeanModel {
beatInterval = this.retryInterval; beatInterval = this.retryInterval;
} }
log.warn("monitor", `Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}`); log.warn("monitor", `Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}`);
} else if (bean.status === MAINTENANCE) {
log.warn("monitor", `Monitor #${this.id} '${this.name}': Under Maintenance | Type: ${this.type}`);
} else { } else {
log.warn("monitor", `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type} | Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`); log.warn("monitor", `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type} | Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
} }
@ -836,7 +860,7 @@ class Monitor extends BeanModel {
-- SUM all uptime duration, also trim off the beat out of time window -- SUM all uptime duration, also trim off the beat out of time window
SUM( SUM(
CASE CASE
WHEN (status = 1) WHEN (status = 1 OR status = 3)
THEN THEN
CASE CASE
WHEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 < duration WHEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 < duration
@ -907,11 +931,49 @@ class Monitor extends BeanModel {
// DOWN -> PENDING = this case not exists // DOWN -> PENDING = this case not exists
// DOWN -> DOWN = not important // DOWN -> DOWN = not important
// * DOWN -> UP = important // * DOWN -> UP = important
let isImportant = isFirstBeat || // MAINTENANCE -> MAINTENANCE = not important
// * MAINTENANCE -> UP = important
// * MAINTENANCE -> DOWN = important
// * DOWN -> MAINTENANCE = important
// * UP -> MAINTENANCE = important
return isFirstBeat ||
(previousBeatStatus === DOWN && currentBeatStatus === MAINTENANCE) ||
(previousBeatStatus === UP && currentBeatStatus === MAINTENANCE) ||
(previousBeatStatus === MAINTENANCE && currentBeatStatus === DOWN) ||
(previousBeatStatus === MAINTENANCE && currentBeatStatus === UP) ||
(previousBeatStatus === UP && currentBeatStatus === DOWN) ||
(previousBeatStatus === DOWN && currentBeatStatus === UP) ||
(previousBeatStatus === PENDING && currentBeatStatus === DOWN);
}
/**
* Is this beat important for notifications?
* @param {boolean} isFirstBeat Is this the first beat of this monitor?
* @param {const} previousBeatStatus Status of the previous beat
* @param {const} currentBeatStatus Status of the current beat
* @returns {boolean} True if is an important beat else false
*/
static isImportantForNotification(isFirstBeat, previousBeatStatus, currentBeatStatus) {
// * ? -> ANY STATUS = important [isFirstBeat]
// UP -> PENDING = not important
// * UP -> DOWN = important
// UP -> UP = not important
// PENDING -> PENDING = not important
// * PENDING -> DOWN = important
// PENDING -> UP = not important
// DOWN -> PENDING = this case not exists
// DOWN -> DOWN = not important
// * DOWN -> UP = important
// MAINTENANCE -> MAINTENANCE = not important
// MAINTENANCE -> UP = not important
// * MAINTENANCE -> DOWN = important
// DOWN -> MAINTENANCE = not important
// UP -> MAINTENANCE = not important
return isFirstBeat ||
(previousBeatStatus === MAINTENANCE && currentBeatStatus === DOWN) ||
(previousBeatStatus === UP && currentBeatStatus === DOWN) || (previousBeatStatus === UP && currentBeatStatus === DOWN) ||
(previousBeatStatus === DOWN && currentBeatStatus === UP) || (previousBeatStatus === DOWN && currentBeatStatus === UP) ||
(previousBeatStatus === PENDING && currentBeatStatus === DOWN); (previousBeatStatus === PENDING && currentBeatStatus === DOWN);
return isImportant;
} }
/** /**
@ -1048,6 +1110,26 @@ class Monitor extends BeanModel {
monitorID monitorID
]); ]);
} }
/**
* Check if monitor is under maintenance
* @param {number} monitorID ID of monitor to check
* @returns {Promise<boolean>}
*/
static async isUnderMaintenance(monitorID) {
let activeCondition = Maintenance.getActiveMaintenanceSQLCondition();
const maintenance = await R.getRow(`
SELECT COUNT(*) AS count
FROM monitor_maintenance mm
JOIN maintenance
ON mm.maintenance_id = maintenance.id
AND mm.monitor_id = ?
LEFT JOIN maintenance_timeslot
ON maintenance_timeslot.maintenance_id = maintenance.id
WHERE ${activeCondition}
LIMIT 1`, [ monitorID ]);
return maintenance.count !== 0;
}
} }
module.exports = Monitor; module.exports = Monitor;

View file

@ -2,6 +2,8 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const cheerio = require("cheerio"); const cheerio = require("cheerio");
const { UptimeKumaServer } = require("../uptime-kuma-server"); const { UptimeKumaServer } = require("../uptime-kuma-server");
const jsesc = require("jsesc");
const Maintenance = require("./maintenance");
class StatusPage extends BeanModel { class StatusPage extends BeanModel {
@ -56,13 +58,19 @@ class StatusPage extends BeanModel {
head.append(`<meta property="og:description" content="${description155}" />`); head.append(`<meta property="og:description" content="${description155}" />`);
// Preload data // Preload data
const json = JSON.stringify(await StatusPage.getStatusPageData(statusPage)); // Add jsesc, fix https://github.com/louislam/uptime-kuma/issues/2186
head.append(` const escapedJSONObject = jsesc(await StatusPage.getStatusPageData(statusPage), {
<script> "isScriptContext": true
window.preloadData = ${json} });
const script = $(`
<script id="preload-data" data-json="{}">
window.preloadData = ${escapedJSONObject};
</script> </script>
`); `);
head.append(script);
// manifest.json // manifest.json
$("link[rel=manifest]").attr("href", `/api/status-page/${statusPage.slug}/manifest.json`); $("link[rel=manifest]").attr("href", `/api/status-page/${statusPage.slug}/manifest.json`);
@ -83,6 +91,8 @@ class StatusPage extends BeanModel {
incident = incident.toPublicJSON(); incident = incident.toPublicJSON();
} }
let maintenanceList = await StatusPage.getMaintenanceList(statusPage.id);
// Public Group List // Public Group List
const publicGroupList = []; const publicGroupList = [];
const showTags = !!statusPage.show_tags; const showTags = !!statusPage.show_tags;
@ -100,7 +110,8 @@ class StatusPage extends BeanModel {
return { return {
config: await statusPage.toPublicJSON(), config: await statusPage.toPublicJSON(),
incident, incident,
publicGroupList publicGroupList,
maintenanceList,
}; };
} }
@ -259,6 +270,36 @@ class StatusPage extends BeanModel {
} }
} }
/**
* Get list of maintenances
* @param {number} statusPageId ID of status page to get maintenance for
* @returns {Object} Object representing maintenances sanitized for public
*/
static async getMaintenanceList(statusPageId) {
try {
const publicMaintenanceList = [];
let activeCondition = Maintenance.getActiveMaintenanceSQLCondition();
let maintenanceBeanList = R.convertToBeans("maintenance", await R.getAll(`
SELECT maintenance.*
FROM maintenance, maintenance_status_page msp, maintenance_timeslot
WHERE msp.maintenance_id = maintenance.id
AND maintenance_timeslot.maintenance_id = maintenance.id
AND msp.status_page_id = ?
AND ${activeCondition}
ORDER BY maintenance.end_date
`, [ statusPageId ]));
for (const bean of maintenanceBeanList) {
publicMaintenanceList.push(await bean.toPublicJSON());
}
return publicMaintenanceList;
} catch (error) {
return [];
}
}
} }
module.exports = StatusPage; module.exports = StatusPage;

View file

@ -0,0 +1,24 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class FreeMobile extends NotificationProvider {
name = "FreeMobile";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
await axios.post(`https://smsapi.free-mobile.fr/sendmsg?msg=${encodeURIComponent(msg.replace("🔴", "⛔️"))}`, {
"user": notification.freemobileUser,
"pass": notification.freemobilePass,
});
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = FreeMobile;

View file

@ -7,18 +7,26 @@ class Ntfy extends NotificationProvider {
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully."; let okMsg = "Sent Successfully.";
var ntfyparams = { try {
let headers = {};
if (notification.ntfyusername) {
headers = {
"Authorization": "Basic " + Buffer.from(notification.ntfyusername + ":" + notification.ntfypassword).toString("base64"),
};
}
let data = {
"topic": notification.ntfytopic, "topic": notification.ntfytopic,
"message": msg, "message": msg,
"priority": notification.ntfyPriority || 4, "priority": notification.ntfyPriority || 4,
"title": "Uptime-Kuma", "title": "Uptime-Kuma",
}; };
if (notification.ntfyIcon) { if (notification.ntfyIcon) {
ntfyparams.icon = notification.ntfyIcon; data.icon = notification.ntfyIcon;
} }
try { await axios.post(`${notification.ntfyserverurl}`, data, { headers: headers });
await axios.post(`${notification.ntfyserverurl}`, ntfyparams);
return okMsg; return okMsg;
} catch (error) { } catch (error) {

View file

@ -10,7 +10,7 @@ class Octopush extends NotificationProvider {
try { try {
// Default - V2 // Default - V2
if (notification.octopushVersion === 2 || !notification.octopushVersion) { if (notification.octopushVersion === "2" || !notification.octopushVersion) {
let config = { let config = {
headers: { headers: {
"api-key": notification.octopushAPIKey, "api-key": notification.octopushAPIKey,
@ -31,7 +31,7 @@ class Octopush extends NotificationProvider {
"sender": notification.octopushSenderName "sender": notification.octopushSenderName
}; };
await axios.post("https://api.octopush.com/v1/public/sms-campaign/send", data, config); await axios.post("https://api.octopush.com/v1/public/sms-campaign/send", data, config);
} else if (notification.octopushVersion === 1) { } else if (notification.octopushVersion === "1") {
let data = { let data = {
"user_login": notification.octopushDMLogin, "user_login": notification.octopushDMLogin,
"api_key": notification.octopushDMAPIKey, "api_key": notification.octopushDMAPIKey,
@ -49,7 +49,15 @@ class Octopush extends NotificationProvider {
}, },
params: data params: data
}; };
await axios.post("https://www.octopush-dm.com/api/sms/json", {}, config);
// V1 API returns 200 even on error so we must check
// response data
let response = await axios.post("https://www.octopush-dm.com/api/sms/json", {}, config);
if ("error_code" in response.data) {
if (response.data.error_code !== "000") {
this.throwGeneralAxiosError(`Octopush error ${JSON.stringify(response.data)}`);
}
}
} else { } else {
throw new Error("Unknown Octopush version!"); throw new Error("Unknown Octopush version!");
} }

View file

@ -0,0 +1,71 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class SMSEagle extends NotificationProvider {
name = "SMSEagle";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
let config = {
headers: {
"Content-Type": "application/json",
}
};
let postData;
let sendMethod;
let recipientType;
let encoding = (notification.smseagleEncoding) ? "1" : "0";
let priority = (notification.smseaglePriority) ? notification.smseaglePriority : "0";
if (notification.smseagleRecipientType === "smseagle-contact") {
recipientType = "contactname";
sendMethod = "sms.send_tocontact";
}
if (notification.smseagleRecipientType === "smseagle-group") {
recipientType = "groupname";
sendMethod = "sms.send_togroup";
}
if (notification.smseagleRecipientType === "smseagle-to") {
recipientType = "to";
sendMethod = "sms.send_sms";
}
let params = {
access_token: notification.smseagleToken,
[recipientType]: notification.smseagleRecipient,
message: msg,
responsetype: "extended",
unicode: encoding,
highpriority: priority
};
postData = {
method: sendMethod,
params: params
};
let resp = await axios.post(notification.smseagleUrl + "/jsonrpc/sms", postData, config);
if ((JSON.stringify(resp.data)).indexOf("message_id") === -1) {
let error = "";
if (resp.data.result && resp.data.result.error_text) {
error = `SMSEagle API returned error: ${JSON.stringify(resp.data.result.error_text)}`;
} else {
error = "SMSEagle API returned an unexpected response";
}
throw new Error(error);
}
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = SMSEagle;

View file

@ -0,0 +1,76 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN } = require("../../src/util");
class Squadcast extends NotificationProvider {
name = "squadcast";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
let config = {};
let data = {
message: msg,
description: "",
tags: {},
heartbeat: heartbeatJSON,
source: "uptime-kuma"
};
if (heartbeatJSON !== null) {
data.description = heartbeatJSON["msg"];
data.event_id = heartbeatJSON["monitorID"];
if (heartbeatJSON["status"] === DOWN) {
data.message = `${monitorJSON["name"]} is DOWN`;
data.status = "trigger";
} else {
data.message = `${monitorJSON["name"]} is UP`;
data.status = "resolve";
}
let address;
switch (monitorJSON["type"]) {
case "ping":
address = monitorJSON["hostname"];
break;
case "port":
case "dns":
case "steam":
address = monitorJSON["hostname"];
if (monitorJSON["port"]) {
address += ":" + monitorJSON["port"];
}
break;
default:
address = monitorJSON["url"];
break;
}
data.tags["AlertAddress"] = address;
monitorJSON["tags"].forEach(tag => {
data.tags[tag["name"]] = {
value: tag["value"]
};
if (tag["color"] !== null) {
data.tags[tag["name"]]["color"] = tag["color"];
}
});
}
await axios.post(notification.squadcastWebhookURL, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Squadcast;

View file

@ -63,7 +63,7 @@ class Teams extends NotificationProvider {
}); });
} }
if (monitorUrl) { if (monitorUrl && monitorUrl !== "https://") {
facts.push({ facts.push({
name: "URL", name: "URL",
value: monitorUrl, value: monitorUrl,
@ -127,13 +127,17 @@ class Teams extends NotificationProvider {
let url; let url;
if (monitorJSON["type"] === "port") { switch (monitorJSON["type"]) {
url = monitorJSON["hostname"]; case "http":
if (monitorJSON["port"]) { case "keywork":
url += ":" + monitorJSON["port"];
}
} else {
url = monitorJSON["url"]; url = monitorJSON["url"];
break;
case "docker":
url = monitorJSON["docker_host"];
break;
default:
url = monitorJSON["hostname"];
break;
} }
const payload = this._notificationPayloadFactory({ const payload = this._notificationPayloadFactory({

View file

@ -9,6 +9,7 @@ const ClickSendSMS = require("./notification-providers/clicksendsms");
const DingDing = require("./notification-providers/dingding"); const DingDing = require("./notification-providers/dingding");
const Discord = require("./notification-providers/discord"); const Discord = require("./notification-providers/discord");
const Feishu = require("./notification-providers/feishu"); const Feishu = require("./notification-providers/feishu");
const FreeMobile = require("./notification-providers/freemobile");
const GoogleChat = require("./notification-providers/google-chat"); const GoogleChat = require("./notification-providers/google-chat");
const Gorush = require("./notification-providers/gorush"); const Gorush = require("./notification-providers/gorush");
const Gotify = require("./notification-providers/gotify"); const Gotify = require("./notification-providers/gotify");
@ -31,7 +32,9 @@ const RocketChat = require("./notification-providers/rocket-chat");
const SerwerSMS = require("./notification-providers/serwersms"); const SerwerSMS = require("./notification-providers/serwersms");
const Signal = require("./notification-providers/signal"); const Signal = require("./notification-providers/signal");
const Slack = require("./notification-providers/slack"); const Slack = require("./notification-providers/slack");
const SMSEagle = require("./notification-providers/smseagle");
const SMTP = require("./notification-providers/smtp"); const SMTP = require("./notification-providers/smtp");
const Squadcast = require("./notification-providers/squadcast");
const Stackfield = require("./notification-providers/stackfield"); const Stackfield = require("./notification-providers/stackfield");
const Teams = require("./notification-providers/teams"); const Teams = require("./notification-providers/teams");
const TechulusPush = require("./notification-providers/techulus-push"); const TechulusPush = require("./notification-providers/techulus-push");
@ -62,6 +65,7 @@ class Notification {
new DingDing(), new DingDing(),
new Discord(), new Discord(),
new Feishu(), new Feishu(),
new FreeMobile(),
new GoogleChat(), new GoogleChat(),
new Gorush(), new Gorush(),
new Gotify(), new Gotify(),
@ -86,7 +90,9 @@ class Notification {
new Signal(), new Signal(),
new SMSManager(), new SMSManager(),
new Slack(), new Slack(),
new SMSEagle(),
new SMTP(), new SMTP(),
new Squadcast(),
new Stackfield(), new Stackfield(),
new Teams(), new Teams(),
new TechulusPush(), new TechulusPush(),

View file

@ -105,7 +105,7 @@ Ping.prototype.send = function (callback) {
let _exited; let _exited;
let _errored; let _errored;
this._ping = spawn(this._bin, this._args); // spawn the binary this._ping = spawn(this._bin, this._args, { windowsHide: true }); // spawn the binary
this._ping.on("error", function (err) { // handle binary errors this._ping.on("error", function (err) { // handle binary errors
_errored = true; _errored = true;

View file

@ -4,7 +4,7 @@ const { R } = require("redbean-node");
const apicache = require("../modules/apicache"); const apicache = require("../modules/apicache");
const Monitor = require("../model/monitor"); const Monitor = require("../model/monitor");
const dayjs = require("dayjs"); const dayjs = require("dayjs");
const { UP, DOWN, flipStatus, log } = require("../../src/util"); const { UP, MAINTENANCE, DOWN, flipStatus, log } = require("../../src/util");
const StatusPage = require("../model/status_page"); const StatusPage = require("../model/status_page");
const { UptimeKumaServer } = require("../uptime-kuma-server"); const { UptimeKumaServer } = require("../uptime-kuma-server");
const { makeBadge } = require("badge-maker"); const { makeBadge } = require("badge-maker");
@ -67,6 +67,11 @@ router.get("/api/push/:pushToken", async (request, response) => {
duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second"); duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second");
} }
if (await Monitor.isUnderMaintenance(monitor.id)) {
msg = "Monitor under maintenance";
status = MAINTENANCE;
}
log.debug("router", `/api/push/ called at ${dayjs().format("YYYY-MM-DD HH:mm:ss.SSS")}`); log.debug("router", `/api/push/ called at ${dayjs().format("YYYY-MM-DD HH:mm:ss.SSS")}`);
log.debug("router", "PreviousStatus: " + previousStatus); log.debug("router", "PreviousStatus: " + previousStatus);
log.debug("router", "Current Status: " + status); log.debug("router", "Current Status: " + status);
@ -87,7 +92,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
ok: true, ok: true,
}); });
if (bean.important) { if (Monitor.isImportantForNotification(isFirstBeat, previousStatus, status)) {
await Monitor.sendNotification(isFirstBeat, monitor, bean); await Monitor.sendNotification(isFirstBeat, monitor, bean);
} }

View file

@ -5,6 +5,12 @@
*/ */
console.log("Welcome to Uptime Kuma"); console.log("Welcome to Uptime Kuma");
// As the log function need to use dayjs, it should be very top
const dayjs = require("dayjs");
dayjs.extend(require("dayjs/plugin/utc"));
dayjs.extend(require("dayjs/plugin/timezone"));
dayjs.extend(require("dayjs/plugin/customParseFormat"));
// Check Node.js Version // Check Node.js Version
const nodeVersion = parseInt(process.versions.node.split(".")[0]); const nodeVersion = parseInt(process.versions.node.split(".")[0]);
const requiredVersion = 14; const requiredVersion = 14;
@ -33,6 +39,7 @@ log.info("server", "Importing Node libraries");
const fs = require("fs"); const fs = require("fs");
log.info("server", "Importing 3rd-party libraries"); log.info("server", "Importing 3rd-party libraries");
log.debug("server", "Importing express"); log.debug("server", "Importing express");
const express = require("express"); const express = require("express");
const expressStaticGzip = require("express-static-gzip"); const expressStaticGzip = require("express-static-gzip");
@ -127,6 +134,8 @@ const StatusPage = require("./model/status_page");
const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart, stop: cloudflaredStop } = require("./socket-handlers/cloudflared-socket-handler"); const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart, stop: cloudflaredStop } = require("./socket-handlers/cloudflared-socket-handler");
const { proxySocketHandler } = require("./socket-handlers/proxy-socket-handler"); const { proxySocketHandler } = require("./socket-handlers/proxy-socket-handler");
const { dockerSocketHandler } = require("./socket-handlers/docker-socket-handler"); const { dockerSocketHandler } = require("./socket-handlers/docker-socket-handler");
const { maintenanceSocketHandler } = require("./socket-handlers/maintenance-socket-handler");
const { Settings } = require("./settings");
app.use(express.json()); app.use(express.json());
@ -154,8 +163,9 @@ let needSetup = false;
(async () => { (async () => {
Database.init(args); Database.init(args);
await initDatabase(testMode); await initDatabase(testMode);
await server.initAfterDatabaseReady();
exports.entryPage = await setting("entryPage"); server.entryPage = await Settings.get("entryPage");
await StatusPage.loadDomainMappingList(); await StatusPage.loadDomainMappingList();
log.info("server", "Adding route"); log.info("server", "Adding route");
@ -176,14 +186,15 @@ let needSetup = false;
log.debug("entry", `Request Domain: ${hostname}`); log.debug("entry", `Request Domain: ${hostname}`);
const uptimeKumaEntryPage = server.entryPage;
if (hostname in StatusPage.domainMappingList) { if (hostname in StatusPage.domainMappingList) {
log.debug("entry", "This is a status page domain"); log.debug("entry", "This is a status page domain");
let slug = StatusPage.domainMappingList[hostname]; let slug = StatusPage.domainMappingList[hostname];
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug); await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
} else if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) { } else if (uptimeKumaEntryPage && uptimeKumaEntryPage.startsWith("statusPage-")) {
response.redirect("/status/" + exports.entryPage.replace("statusPage-", "")); response.redirect("/status/" + uptimeKumaEntryPage.replace("statusPage-", ""));
} else { } else {
response.redirect("/dashboard"); response.redirect("/dashboard");
@ -200,7 +211,7 @@ let needSetup = false;
// Robots.txt // Robots.txt
app.get("/robots.txt", async (_request, response) => { app.get("/robots.txt", async (_request, response) => {
let txt = "User-agent: *\nDisallow:"; let txt = "User-agent: *\nDisallow:";
if (! await setting("searchEngineIndex")) { if (!await setting("searchEngineIndex")) {
txt += " /"; txt += " /";
} }
response.setHeader("Content-Type", "text/plain"); response.setHeader("Content-Type", "text/plain");
@ -1055,10 +1066,15 @@ let needSetup = false;
socket.on("getSettings", async (callback) => { socket.on("getSettings", async (callback) => {
try { try {
checkLogin(socket); checkLogin(socket);
const data = await getSettings("general");
if (!data.serverTimezone) {
data.serverTimezone = await server.getTimezone();
}
callback({ callback({
ok: true, ok: true,
data: await getSettings("general"), data: data,
}); });
} catch (e) { } catch (e) {
@ -1084,7 +1100,12 @@ let needSetup = false;
} }
await setSettings("general", data); await setSettings("general", data);
exports.entryPage = data.entryPage; server.entryPage = data.entryPage;
// Also need to apply timezone globally
if (data.serverTimezone) {
await server.setTimezone(data.serverTimezone);
}
callback({ callback({
ok: true, ok: true,
@ -1092,6 +1113,7 @@ let needSetup = false;
}); });
sendInfo(socket); sendInfo(socket);
server.sendMaintenanceList(socket);
} catch (e) { } catch (e) {
callback({ callback({
@ -1450,6 +1472,7 @@ let needSetup = false;
databaseSocketHandler(socket); databaseSocketHandler(socket);
proxySocketHandler(socket); proxySocketHandler(socket);
dockerSocketHandler(socket); dockerSocketHandler(socket);
maintenanceSocketHandler(socket);
log.debug("server", "added all socket handlers"); log.debug("server", "added all socket handlers");
@ -1552,6 +1575,7 @@ async function afterLogin(socket, user) {
socket.join(user.id); socket.join(user.id);
let monitorList = await server.sendMonitorList(socket); let monitorList = await server.sendMonitorList(socket);
server.sendMaintenanceList(socket);
sendNotificationList(socket); sendNotificationList(socket);
sendProxyList(socket); sendProxyList(socket);
sendDockerHostList(socket); sendDockerHostList(socket);
@ -1697,6 +1721,8 @@ async function shutdownFunction(signal) {
log.info("server", "Shutdown requested"); log.info("server", "Shutdown requested");
log.info("server", "Called signal: " + signal); log.info("server", "Called signal: " + signal);
await server.stop();
log.info("server", "Stopping all monitors"); log.info("server", "Stopping all monitors");
for (let id in server.monitorList) { for (let id in server.monitorList) {
let monitor = server.monitorList[id]; let monitor = server.monitorList[id];

View file

@ -56,7 +56,7 @@ module.exports.dockerSocketHandler = (socket) => {
let amount = await DockerHost.testDockerHost(dockerHost); let amount = await DockerHost.testDockerHost(dockerHost);
let msg; let msg;
if (amount > 1) { if (amount >= 1) {
msg = "Connected Successfully. Amount of containers: " + amount; msg = "Connected Successfully. Amount of containers: " + amount;
} else { } else {
msg = "Connected Successfully, but there are no containers?"; msg = "Connected Successfully, but there are no containers?";

View file

@ -0,0 +1,311 @@
const { checkLogin } = require("../util-server");
const { log } = require("../../src/util");
const { R } = require("redbean-node");
const apicache = require("../modules/apicache");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const Maintenance = require("../model/maintenance");
const server = UptimeKumaServer.getInstance();
const MaintenanceTimeslot = require("../model/maintenance_timeslot");
/**
* Handlers for Maintenance
* @param {Socket} socket Socket.io instance
*/
module.exports.maintenanceSocketHandler = (socket) => {
// Add a new maintenance
socket.on("addMaintenance", async (maintenance, callback) => {
try {
checkLogin(socket);
log.debug("maintenance", maintenance);
let bean = Maintenance.jsonToBean(R.dispense("maintenance"), maintenance);
bean.user_id = socket.userID;
let maintenanceID = await R.store(bean);
await MaintenanceTimeslot.generateTimeslot(bean);
await server.sendMaintenanceList(socket);
callback({
ok: true,
msg: "Added Successfully.",
maintenanceID,
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
// Edit a maintenance
socket.on("editMaintenance", async (maintenance, callback) => {
try {
checkLogin(socket);
let bean = await R.findOne("maintenance", " id = ? ", [ maintenance.id ]);
if (bean.user_id !== socket.userID) {
throw new Error("Permission denied.");
}
Maintenance.jsonToBean(bean, maintenance);
await R.store(bean);
await MaintenanceTimeslot.generateTimeslot(bean, null, true);
await server.sendMaintenanceList(socket);
callback({
ok: true,
msg: "Saved.",
maintenanceID: bean.id,
});
} catch (e) {
console.error(e);
callback({
ok: false,
msg: e.message,
});
}
});
// Add a new monitor_maintenance
socket.on("addMonitorMaintenance", async (maintenanceID, monitors, callback) => {
try {
checkLogin(socket);
await R.exec("DELETE FROM monitor_maintenance WHERE maintenance_id = ?", [
maintenanceID
]);
for await (const monitor of monitors) {
let bean = R.dispense("monitor_maintenance");
bean.import({
monitor_id: monitor.id,
maintenance_id: maintenanceID
});
await R.store(bean);
}
apicache.clear();
callback({
ok: true,
msg: "Added Successfully.",
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
// Add a new monitor_maintenance
socket.on("addMaintenanceStatusPage", async (maintenanceID, statusPages, callback) => {
try {
checkLogin(socket);
await R.exec("DELETE FROM maintenance_status_page WHERE maintenance_id = ?", [
maintenanceID
]);
for await (const statusPage of statusPages) {
let bean = R.dispense("maintenance_status_page");
bean.import({
status_page_id: statusPage.id,
maintenance_id: maintenanceID
});
await R.store(bean);
}
apicache.clear();
callback({
ok: true,
msg: "Added Successfully.",
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("getMaintenance", async (maintenanceID, callback) => {
try {
checkLogin(socket);
log.debug("maintenance", `Get Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
let bean = await R.findOne("maintenance", " id = ? AND user_id = ? ", [
maintenanceID,
socket.userID,
]);
callback({
ok: true,
maintenance: await bean.toJSON(),
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("getMaintenanceList", async (callback) => {
try {
checkLogin(socket);
await server.sendMaintenanceList(socket);
callback({
ok: true,
});
} catch (e) {
console.error(e);
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("getMonitorMaintenance", async (maintenanceID, callback) => {
try {
checkLogin(socket);
log.debug("maintenance", `Get Monitors for Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
let monitors = await R.getAll("SELECT monitor.id, monitor.name FROM monitor_maintenance mm JOIN monitor ON mm.monitor_id = monitor.id WHERE mm.maintenance_id = ? ", [
maintenanceID,
]);
callback({
ok: true,
monitors,
});
} catch (e) {
console.error(e);
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("getMaintenanceStatusPage", async (maintenanceID, callback) => {
try {
checkLogin(socket);
log.debug("maintenance", `Get Status Pages for Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
let statusPages = await R.getAll("SELECT status_page.id, status_page.title FROM maintenance_status_page msp JOIN status_page ON msp.status_page_id = status_page.id WHERE msp.maintenance_id = ? ", [
maintenanceID,
]);
callback({
ok: true,
statusPages,
});
} catch (e) {
console.error(e);
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("deleteMaintenance", async (maintenanceID, callback) => {
try {
checkLogin(socket);
log.debug("maintenance", `Delete Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
if (maintenanceID in server.maintenanceList) {
delete server.maintenanceList[maintenanceID];
}
await R.exec("DELETE FROM maintenance WHERE id = ? AND user_id = ? ", [
maintenanceID,
socket.userID,
]);
callback({
ok: true,
msg: "Deleted Successfully.",
});
await server.sendMaintenanceList(socket);
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("pauseMaintenance", async (maintenanceID, callback) => {
try {
checkLogin(socket);
log.debug("maintenance", `Pause Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
await R.exec("UPDATE maintenance SET active = 0 WHERE id = ? ", [
maintenanceID,
]);
callback({
ok: true,
msg: "Paused Successfully.",
});
await server.sendMaintenanceList(socket);
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("resumeMaintenance", async (maintenanceID, callback) => {
try {
checkLogin(socket);
log.debug("maintenance", `Resume Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
await R.exec("UPDATE maintenance SET active = 1 WHERE id = ? ", [
maintenanceID,
]);
callback({
ok: true,
msg: "Resume Successfully",
});
await server.sendMaintenanceList(socket);
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
};

View file

@ -9,6 +9,8 @@ const Database = require("./database");
const util = require("util"); const util = require("util");
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent"); const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
const { Settings } = require("./settings"); const { Settings } = require("./settings");
const dayjs = require("dayjs");
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`
/** /**
* `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue. * `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
@ -26,6 +28,13 @@ class UptimeKumaServer {
* @type {{}} * @type {{}}
*/ */
monitorList = {}; monitorList = {};
/**
* Main maintenance list
* @type {{}}
*/
maintenanceList = {};
entryPage = "dashboard"; entryPage = "dashboard";
app = undefined; app = undefined;
httpServer = undefined; httpServer = undefined;
@ -37,6 +46,8 @@ class UptimeKumaServer {
*/ */
indexHTML = ""; indexHTML = "";
generateMaintenanceTimeslotsInterval = undefined;
static getInstance(args) { static getInstance(args) {
if (UptimeKumaServer.instance == null) { if (UptimeKumaServer.instance == null) {
UptimeKumaServer.instance = new UptimeKumaServer(args); UptimeKumaServer.instance = new UptimeKumaServer(args);
@ -77,6 +88,16 @@ class UptimeKumaServer {
this.io = new Server(this.httpServer); this.io = new Server(this.httpServer);
} }
async initAfterDatabaseReady() {
process.env.TZ = await this.getTimezone();
dayjs.tz.setDefault(process.env.TZ);
log.debug("DEBUG", "Timezone: " + process.env.TZ);
log.debug("DEBUG", "Current Time: " + dayjs.tz().format());
await this.generateMaintenanceTimeslots();
this.generateMaintenanceTimeslotsInterval = setInterval(this.generateMaintenanceTimeslots, 60 * 1000);
}
async sendMonitorList(socket) { async sendMonitorList(socket) {
let list = await this.getMonitorJSONList(socket.userID); let list = await this.getMonitorJSONList(socket.userID);
this.io.to(socket.userID).emit("monitorList", list); this.io.to(socket.userID).emit("monitorList", list);
@ -104,6 +125,40 @@ class UptimeKumaServer {
return result; return result;
} }
/**
* Send maintenance list to client
* @param {Socket} socket Socket.io instance to send to
* @returns {Object}
*/
async sendMaintenanceList(socket) {
return await this.sendMaintenanceListByUserID(socket.userID);
}
async sendMaintenanceListByUserID(userID) {
let list = await this.getMaintenanceJSONList(userID);
this.io.to(userID).emit("maintenanceList", list);
return list;
}
/**
* Get a list of maintenances for the given user.
* @param {string} userID - The ID of the user to get maintenances for.
* @returns {Promise<Object>} A promise that resolves to an object with maintenance IDs as keys and maintenances objects as values.
*/
async getMaintenanceJSONList(userID) {
let result = {};
let maintenanceList = await R.find("maintenance", " user_id = ? ORDER BY end_date DESC, title", [
userID,
]);
for (let maintenance of maintenanceList) {
result[maintenance.id] = await maintenance.toJSON();
}
return result;
}
/** /**
* Write error to log file * Write error to log file
* @param {any} error The error to write * @param {any} error The error to write
@ -138,15 +193,58 @@ class UptimeKumaServer {
} }
if (await Settings.get("trustProxy")) { if (await Settings.get("trustProxy")) {
return socket.client.conn.request.headers["x-forwarded-for"] const forwardedFor = socket.client.conn.request.headers["x-forwarded-for"];
return (typeof forwardedFor === "string" ? forwardedFor.split(",")[0].trim() : null)
|| socket.client.conn.request.headers["x-real-ip"] || socket.client.conn.request.headers["x-real-ip"]
|| clientIP.replace(/^.*:/, ""); || clientIP.replace(/^.*:/, "");
} else { } else {
return clientIP.replace(/^.*:/, ""); return clientIP.replace(/^.*:/, "");
} }
} }
async getTimezone() {
let timezone = await Settings.get("serverTimezone");
if (timezone) {
return timezone;
} else if (process.env.TZ) {
return process.env.TZ;
} else {
return dayjs.tz.guess();
}
}
getTimezoneOffset() {
return dayjs().format("Z");
}
async setTimezone(timezone) {
await Settings.set("serverTimezone", timezone, "general");
process.env.TZ = timezone;
dayjs.tz.setDefault(timezone);
}
async generateMaintenanceTimeslots() {
let list = await R.find("maintenance_timeslot", " generated_next = 0 AND start_date <= DATETIME('now') ");
for (let maintenanceTimeslot of list) {
let maintenance = await maintenanceTimeslot.maintenance;
await MaintenanceTimeslot.generateTimeslot(maintenance, maintenanceTimeslot.end_date, false);
maintenanceTimeslot.generated_next = true;
await R.store(maintenanceTimeslot);
}
}
async stop() {
clearTimeout(this.generateMaintenanceTimeslotsInterval);
}
} }
module.exports = { module.exports = {
UptimeKumaServer UptimeKumaServer
}; };
// Must be at the end
const MaintenanceTimeslot = require("./model/maintenance_timeslot");

View file

@ -21,6 +21,7 @@ const {
rfc2865: { file, attributes }, rfc2865: { file, attributes },
}, },
} = require("node-radius-utils"); } = require("node-radius-utils");
const dayjs = require("dayjs");
// From ping-lite // From ping-lite
exports.WIN = /^win/.test(process.platform); exports.WIN = /^win/.test(process.platform);
@ -291,6 +292,17 @@ exports.postgresQuery = function (connectionString, query) {
}); });
}; };
/**
* Query radius server
* @param {string} hostname Hostname of radius server
* @param {string} username Username to use
* @param {string} password Password to use
* @param {string} calledStationId ID of called station
* @param {string} callingStationId ID of calling station
* @param {string} secret Secret to use
* @param {number} [port=1812] Port to contact radius server on
* @returns {Promise<any>}
*/
exports.radius = function ( exports.radius = function (
hostname, hostname,
username, username,
@ -298,9 +310,11 @@ exports.radius = function (
calledStationId, calledStationId,
callingStationId, callingStationId,
secret, secret,
port = 1812,
) { ) {
const client = new radiusClient({ const client = new radiusClient({
host: hostname, host: hostname,
hostPort: port,
dictionaries: [ file ], dictionaries: [ file ],
}); });
@ -557,7 +571,7 @@ exports.doubleCheckPassword = async (socket, currentPassword) => {
exports.startUnitTest = async () => { exports.startUnitTest = async () => {
console.log("Starting unit test..."); console.log("Starting unit test...");
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm"; const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
const child = childProcess.spawn(npm, [ "run", "jest" ]); const child = childProcess.spawn(npm, [ "run", "jest-backend" ]);
child.stdout.on("data", (data) => { child.stdout.on("data", (data) => {
console.log(data.toString()); console.log(data.toString());
@ -645,3 +659,64 @@ module.exports.send403 = (res, msg = "") => {
"msg": msg, "msg": msg,
}); });
}; };
function timeObjectConvertTimezone(obj, timezone, timeObjectToUTC = true) {
let offsetString;
if (timezone) {
offsetString = dayjs().tz(timezone).format("Z");
} else {
offsetString = dayjs().format("Z");
}
let hours = parseInt(offsetString.substring(1, 3));
let minutes = parseInt(offsetString.substring(4, 6));
if (
(timeObjectToUTC && offsetString.startsWith("+")) ||
(!timeObjectToUTC && offsetString.startsWith("-"))
) {
hours *= -1;
minutes *= -1;
}
obj.hours += hours;
obj.minutes += minutes;
// Handle out of bound
if (obj.minutes < 0) {
obj.minutes += 60;
obj.hours--;
} else if (obj.minutes > 60) {
obj.minutes -= 60;
obj.hours++;
}
if (obj.hours < 0) {
obj.hours += 24;
} else if (obj.hours > 24) {
obj.hours -= 24;
}
return obj;
}
/**
*
* @param {object} obj
* @param {string} timezone
* @returns {object}
*/
module.exports.timeObjectToUTC = (obj, timezone = undefined) => {
return timeObjectConvertTimezone(obj, timezone, true);
};
/**
*
* @param {object} obj
* @param {string} timezone
* @returns {object}
*/
module.exports.timeObjectToLocal = (obj, timezone = undefined) => {
return timeObjectConvertTimezone(obj, timezone, false);
};

View file

@ -22,6 +22,19 @@ textarea.form-control {
width: 10px; width: 10px;
} }
.bg-maintenance {
color: white !important;
background-color: $maintenance !important;
}
.bg-dark {
color: white;
}
.text-maintenance {
color: $maintenance !important;
}
.list-group { .list-group {
border-radius: 0.75rem; border-radius: 0.75rem;
@ -107,6 +120,19 @@ optgroup {
} }
} }
.btn-normal {
$bg-color: #F5F5F5;
background-color: $bg-color;
border-color: $bg-color;
&:hover {
$hover-color: darken($bg-color, 3%);
background-color: $hover-color;
border-color: $hover-color;
}
}
.btn-warning { .btn-warning {
color: white; color: white;
@ -256,6 +282,20 @@ optgroup {
color: white; color: white;
} }
.btn-normal {
$bg-color: $dark-header-bg;
color: $dark-font-color;
background-color: $bg-color;
border-color: $bg-color;
&:hover {
$hover-color: darken($bg-color, 3%);
background-color: $hover-color;
border-color: $hover-color;
}
}
.btn-warning { .btn-warning {
color: $dark-font-color2; color: $dark-font-color2;
@ -323,6 +363,7 @@ optgroup {
&.bg-info, &.bg-info,
&.bg-warning, &.bg-warning,
&.bg-danger, &.bg-danger,
&.bg-maintenance,
&.bg-light { &.bg-light {
color: $dark-font-color2; color: $dark-font-color2;
} }

View file

@ -1,6 +1,7 @@
$primary: #5cdd8b; $primary: #5cdd8b;
$danger: #dc3545; $danger: #dc3545;
$warning: #f8a306; $warning: #f8a306;
$maintenance: #1747f5;
$link-color: #111; $link-color: #111;
$border-radius: 50rem; $border-radius: 50rem;

View file

@ -0,0 +1,39 @@
@import "@vuepic/vue-datepicker/dist/main.css";
@import "vars.scss";
// Must use #{ }
// Remark: https://stackoverflow.com/questions/50202991/unable-to-set-scss-variable-to-css-variable
.dp__theme_dark {
--dp-background-color: #{$dark-bg2};
--dp-text-color: #{$dark-font-color};
--dp-hover-color: #484848;
--dp-hover-text-color: #ffffff;
--dp-hover-icon-color: #959595;
--dp-primary-color: #{#5cdd8b};
--dp-primary-text-color: #ffffff;
--dp-secondary-color: #494949;
--dp-border-color: #{$dark-border-color};
--dp-menu-border-color: #2d2d2d;
--dp-border-color-hover: #{$dark-border-color};
--dp-disabled-color: #212121;
--dp-scroll-bar-background: #212121;
--dp-scroll-bar-color: #484848;
--dp-success-color: #{$primary};
--dp-success-color-disabled: #428f59;
--dp-icon-color: #959595;
--dp-danger-color: #e53935;
--dp-highlight-color: rgba(0, 92, 178, 0.2);
}
.dp__input {
border-radius: $border-radius;
}
// Fix: Full width of text input when using "inline textInput inlineWithInput" mode
.dp__main > div[aria-label="Datepicker input"] {
width: 100%;
}
.dp__main > div[aria-label="Datepicker menu"]:nth-child(2) {
margin-top: 20px;
}

View file

@ -3,14 +3,6 @@
</template> </template>
<script> <script>
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import timezone from "dayjs/plugin/timezone"; // dependent on utc plugin
import utc from "dayjs/plugin/utc";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(relativeTime);
export default { export default {
props: { props: {
/** Value of date time */ /** Value of date time */

View file

@ -30,7 +30,8 @@
{{ $t("Examples") }}: {{ $t("Examples") }}:
<ul> <ul>
<li>/var/run/docker.sock</li> <li>/var/run/docker.sock</li>
<li>tcp://localhost:2375</li> <li>http://localhost:2375</li>
<li>https://localhost:2376 (TLS)</li>
</ul> </ul>
</div> </div>
</div> </div>

View file

@ -5,7 +5,7 @@
v-for="(beat, index) in shortBeatList" v-for="(beat, index) in shortBeatList"
:key="index" :key="index"
class="beat" class="beat"
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0), 'pending' : (beat.status === 2) }" :class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0), 'pending' : (beat.status === 2), 'maintenance' : (beat.status === 3) }"
:style="beatStyle" :style="beatStyle"
:title="getBeatTitle(beat)" :title="getBeatTitle(beat)"
/> />
@ -211,6 +211,10 @@ export default {
background-color: $warning; background-color: $warning;
} }
&.maintenance {
background-color: $maintenance;
}
&:not(.empty):hover { &:not(.empty):hover {
transition: all ease-in-out 0.15s; transition: all ease-in-out 0.15s;
opacity: 0.8; opacity: 0.8;

View file

@ -42,7 +42,7 @@ export default {
/** Should the field auto complete */ /** Should the field auto complete */
autocomplete: { autocomplete: {
type: String, type: String,
default: undefined, default: "new-password",
}, },
/** Is the input required? */ /** Is the input required? */
required: { required: {

View file

@ -0,0 +1,44 @@
<template>
<div>
<div v-if="maintenance.strategy === 'manual'" class="timeslot">
{{ $t("Manual") }}
</div>
<div v-else-if="maintenance.timeslotList.length > 0" class="timeslot">
{{ maintenance.timeslotList[0].startDateServerTimezone }}
<span class="to">-</span>
{{ maintenance.timeslotList[0].endDateServerTimezone }}
(UTC{{ maintenance.timeslotList[0].serverTimezoneOffset }})
</div>
</div>
</template>
<script>
export default {
props: {
maintenance: {
type: Object,
required: true
},
},
};
</script>
<style lang="scss">
.timeslot {
margin-top: 5px;
display: inline-block;
font-size: 14px;
background-color: rgba(255, 255, 255, 0.5);
border-radius: 20px;
padding: 0 10px;
.to {
margin: 0 6px;
}
.dark & {
color: white;
background-color: rgba(255, 255, 255, 0.1);
}
}
</style>

View file

@ -16,18 +16,14 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="js">
import { BarController, BarElement, Chart, Filler, LinearScale, LineController, LineElement, PointElement, TimeScale, Tooltip } from "chart.js"; import { BarController, BarElement, Chart, Filler, LinearScale, LineController, LineElement, PointElement, TimeScale, Tooltip } from "chart.js";
import "chartjs-adapter-dayjs"; import "chartjs-adapter-dayjs";
import dayjs from "dayjs"; import dayjs from "dayjs";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
import { LineChart } from "vue-chart-3"; import { LineChart } from "vue-chart-3";
import { useToast } from "vue-toastification"; import { useToast } from "vue-toastification";
import { DOWN, log } from "../util.ts"; import { DOWN, PENDING, MAINTENANCE, log } from "../util.ts";
dayjs.extend(utc);
dayjs.extend(timezone);
const toast = useToast(); const toast = useToast();
Chart.register(LineController, BarController, LineElement, PointElement, TimeScale, BarElement, LinearScale, Tooltip, Filler); Chart.register(LineController, BarController, LineElement, PointElement, TimeScale, BarElement, LinearScale, Tooltip, Filler);
@ -163,7 +159,8 @@ export default {
}, },
chartData() { chartData() {
let pingData = []; // Ping Data for Line Chart, y-axis contains ping time let pingData = []; // Ping Data for Line Chart, y-axis contains ping time
let downData = []; // Down Data for Bar Chart, y-axis is 1 if target is down, 0 if target is up let downData = []; // Down Data for Bar Chart, y-axis is 1 if target is down (red color), under maintenance (blue color) or pending (orange color), 0 if target is up
let colorData = []; // Color Data for Bar Chart
let heartbeatList = this.heartbeatList || let heartbeatList = this.heartbeatList ||
(this.monitorId in this.$root.heartbeatList && this.$root.heartbeatList[this.monitorId]) || (this.monitorId in this.$root.heartbeatList && this.$root.heartbeatList[this.monitorId]) ||
@ -185,8 +182,9 @@ export default {
}); });
downData.push({ downData.push({
x, x,
y: beat.status === DOWN ? 1 : 0, y: (beat.status === DOWN || beat.status === MAINTENANCE || beat.status === PENDING) ? 1 : 0,
}); });
colorData.push((beat.status === MAINTENANCE) ? "rgba(23,71,245,0.41)" : ((beat.status === PENDING) ? "rgba(245,182,23,0.41)" : "#DC354568"));
}); });
return { return {
@ -205,7 +203,7 @@ export default {
type: "bar", type: "bar",
data: downData, data: downData,
borderColor: "#00000000", borderColor: "#00000000",
backgroundColor: "#DC354568", backgroundColor: colorData,
yAxisID: "y1", yAxisID: "y1",
barThickness: "flex", barThickness: "flex",
barPercentage: 1, barPercentage: 1,

View file

@ -225,4 +225,8 @@ export default {
} }
} }
.bg-maintenance {
background-color: $maintenance;
}
</style> </style>

View file

@ -26,6 +26,10 @@ export default {
return "warning"; return "warning";
} }
if (this.status === 3) {
return "maintenance";
}
return "secondary"; return "secondary";
}, },
@ -42,6 +46,10 @@ export default {
return this.$t("Pending"); return this.$t("Pending");
} }
if (this.status === 3) {
return this.$t("statusMaintenance");
}
return this.$t("Unknown"); return this.$t("Unknown");
}, },
}, },

View file

@ -25,6 +25,10 @@ export default {
computed: { computed: {
uptime() { uptime() {
if (this.type === "maintenance") {
return this.$t("statusMaintenance");
}
let key = this.monitor.id + "_" + this.type; let key = this.monitor.id + "_" + this.type;
if (this.$root.uptimeList[key] !== undefined) { if (this.$root.uptimeList[key] !== undefined) {
@ -35,6 +39,10 @@ export default {
}, },
color() { color() {
if (this.type === "maintenance" || this.monitor.maintenance) {
return "maintenance";
}
if (this.lastHeartBeat.status === 0) { if (this.lastHeartBeat.status === 0) {
return "danger"; return "danger";
} }

View file

@ -6,7 +6,7 @@
</i18n-t> </i18n-t>
<input id="clicksendsms-login" v-model="$parent.notification.clicksendsmsLogin" type="text" class="form-control" required> <input id="clicksendsms-login" v-model="$parent.notification.clicksendsmsLogin" type="text" class="form-control" required>
<label for="clicksendsms-key" class="form-label">{{ $t("API Key") }}</label> <label for="clicksendsms-key" class="form-label">{{ $t("API Key") }}</label>
<HiddenInput id="clicksendsms-key" v-model="$parent.notification.clicksendsmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput> <HiddenInput id="clicksendsms-key" v-model="$parent.notification.clicksendsmsPassword" :required="true" autocomplete="new-password"></HiddenInput>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<div class="form-text"> <div class="form-text">

View file

@ -0,0 +1,12 @@
<template>
<div class="mb-3">
<label for="freemobileUser" class="form-label">{{ $t("Free Mobile User Identifier") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="freemobileUser" v-model="$parent.notification.freemobileUser" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="freemobilePass" class="form-label">{{ $t("Free Mobile API Key") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="freemobilePass" v-model="$parent.notification.freemobilePass" type="text" class="form-control" required>
</div>
</template>

View file

@ -11,7 +11,7 @@
<div class="mb-3"> <div class="mb-3">
<label for="goalert-token" class="form-label">{{ $t("Token") }}</label> <label for="goalert-token" class="form-label">{{ $t("Token") }}</label>
<HiddenInput id="goalert-token" v-model="$parent.notification.goAlertToken" autocomplete="one-time-code" :required="true"></HiddenInput> <HiddenInput id="goalert-token" v-model="$parent.notification.goAlertToken" autocomplete="new-password" :required="true"></HiddenInput>
<div class="form-text"> <div class="form-text">
{{ $t("goAlertIntegrationKeyInfo") }} {{ $t("goAlertIntegrationKeyInfo") }}

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="mb-3"> <div class="mb-3">
<label for="gotify-application-token" class="form-label">{{ $t("Application Token") }}</label> <label for="gotify-application-token" class="form-label">{{ $t("Application Token") }}</label>
<HiddenInput id="gotify-application-token" v-model="$parent.notification.gotifyapplicationToken" :required="true" autocomplete="one-time-code"></HiddenInput> <HiddenInput id="gotify-application-token" v-model="$parent.notification.gotifyapplicationToken" :required="true" autocomplete="new-password"></HiddenInput>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="gotify-server-url" class="form-label">{{ $t("Server URL") }}</label> <label for="gotify-server-url" class="form-label">{{ $t("Server URL") }}</label>

View file

@ -18,7 +18,7 @@
<input id="notificationService" v-model="$parent.notification.notificationService" type="text" :placeholder="$t('default: notify all devices')" class="form-control"> <input id="notificationService" v-model="$parent.notification.notificationService" type="text" :placeholder="$t('default: notify all devices')" class="form-control">
<div class="form-text"> <div class="form-text">
<p>{{ $t("A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.") }}</p> <p>{{ $t('A list of Notification Services can be found in Home Assistant under "Developer Tools > Services" search for "notification" to find your device/phone name.') }}</p>
<p>{{ $t("Automations can optionally be triggered in Home Assistant:") }}</p> <p>{{ $t("Automations can optionally be triggered in Home Assistant:") }}</p>
<p> <p>
{{ $t("Trigger type:") }} <code>Event</code><br /> {{ $t("Trigger type:") }} <code>Event</code><br />

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="mb-3"> <div class="mb-3">
<label for="line-channel-access-token" class="form-label">{{ $t("Channel access token") }}</label> <label for="line-channel-access-token" class="form-label">{{ $t("Channel access token") }}</label>
<HiddenInput id="line-channel-access-token" v-model="$parent.notification.lineChannelAccessToken" :required="true" autocomplete="one-time-code"></HiddenInput> <HiddenInput id="line-channel-access-token" v-model="$parent.notification.lineChannelAccessToken" :required="true" autocomplete="new-password"></HiddenInput>
</div> </div>
<i18n-t tag="div" keypath="lineDevConsoleTo" class="form-text"> <i18n-t tag="div" keypath="lineDevConsoleTo" class="form-text">
<b>{{ $t("Basic Settings") }}</b> <b>{{ $t("Basic Settings") }}</b>

View file

@ -9,7 +9,7 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="access-token" class="form-label">{{ $t("Access Token") }}</label><span style="color: red;"><sup>*</sup></span> <label for="access-token" class="form-label">{{ $t("Access Token") }}</label><span style="color: red;"><sup>*</sup></span>
<HiddenInput id="access-token" v-model="$parent.notification.accessToken" :required="true" autocomplete="one-time-code" :maxlength="500"></HiddenInput> <HiddenInput id="access-token" v-model="$parent.notification.accessToken" :required="true" autocomplete="new-password" :maxlength="500"></HiddenInput>
</div> </div>
<div class="form-text"> <div class="form-text">

View file

@ -11,12 +11,22 @@
<input id="ntfy-server-url" v-model="$parent.notification.ntfyserverurl" type="text" class="form-control" required> <input id="ntfy-server-url" v-model="$parent.notification.ntfyserverurl" type="text" class="form-control" required>
</div> </div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="ntfy-priority" class="form-label">{{ $t("Priority") }}</label> <label for="ntfy-priority" class="form-label">{{ $t("Priority") }}</label>
<input id="ntfy-priority" v-model="$parent.notification.ntfyPriority" type="number" class="form-control" required min="1" max="5" step="1"> <input id="ntfy-priority" v-model="$parent.notification.ntfyPriority" type="number" class="form-control" required min="1" max="5" step="1">
</div> </div>
<div class="mb-3">
<label for="ntfy-username" class="form-label">{{ $t("Username") }} ({{ $t("Optional") }})</label>
<div class="input-group mb-3">
<input id="ntfy-username" v-model="$parent.notification.ntfyusername" type="text" class="form-control">
</div>
</div>
<div class="mb-3">
<label for="ntfy-password" class="form-label">{{ $t("Password") }} ({{ $t("Optional") }})</label>
<div class="input-group mb-3">
<HiddenInput id="ntfy-password" v-model="$parent.notification.ntfypassword" autocomplete="new-password"></HiddenInput>
</div>
</div>
<div class="mb-3"> <div class="mb-3">
<label for="ntfy-icon" class="form-label">{{ $t("IconUrl") }}</label> <label for="ntfy-icon" class="form-label">{{ $t("IconUrl") }}</label>
<input id="ntfy-icon" v-model="$parent.notification.ntfyIcon" type="text" class="form-control"> <input id="ntfy-icon" v-model="$parent.notification.ntfyIcon" type="text" class="form-control">
@ -24,7 +34,12 @@
</template> </template>
<script> <script>
import HiddenInput from "../HiddenInput.vue";
export default { export default {
components: {
HiddenInput,
},
mounted() { mounted() {
if (typeof this.$parent.notification.ntfyPriority === "undefined") { if (typeof this.$parent.notification.ntfyPriority === "undefined") {
this.$parent.notification.ntfyserverurl = "https://ntfy.sh"; this.$parent.notification.ntfyserverurl = "https://ntfy.sh";

View file

@ -11,7 +11,7 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="octopush-key" class="form-label">{{ $t("octopushAPIKey") }}</label> <label for="octopush-key" class="form-label">{{ $t("octopushAPIKey") }}</label>
<HiddenInput id="octopush-key" v-model="$parent.notification.octopushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput> <HiddenInput id="octopush-key" v-model="$parent.notification.octopushAPIKey" :required="true" autocomplete="new-password"></HiddenInput>
<label for="octopush-login" class="form-label">{{ $t("octopushLogin") }}</label> <label for="octopush-login" class="form-label">{{ $t("octopushLogin") }}</label>
<input id="octopush-login" v-model="$parent.notification.octopushLogin" type="text" class="form-control" required> <input id="octopush-login" v-model="$parent.notification.octopushLogin" type="text" class="form-control" required>
</div> </div>

View file

@ -3,7 +3,7 @@
<label for="promosms-login" class="form-label">{{ $t("promosmsLogin") }}</label> <label for="promosms-login" class="form-label">{{ $t("promosmsLogin") }}</label>
<input id="promosms-login" v-model="$parent.notification.promosmsLogin" type="text" class="form-control" required> <input id="promosms-login" v-model="$parent.notification.promosmsLogin" type="text" class="form-control" required>
<label for="promosms-key" class="form-label">{{ $t("promosmsPassword") }}</label> <label for="promosms-key" class="form-label">{{ $t("promosmsPassword") }}</label>
<HiddenInput id="promosms-key" v-model="$parent.notification.promosmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput> <HiddenInput id="promosms-key" v-model="$parent.notification.promosmsPassword" :required="true" autocomplete="new-password"></HiddenInput>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="promosms-type-sms" class="form-label">{{ $t("SMS Type") }}</label> <label for="promosms-type-sms" class="form-label">{{ $t("SMS Type") }}</label>

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="mb-3"> <div class="mb-3">
<label for="pushdeer-key" class="form-label">{{ $t("PushDeer Key") }}</label> <label for="pushdeer-key" class="form-label">{{ $t("PushDeer Key") }}</label>
<HiddenInput id="pushdeer-key" v-model="$parent.notification.pushdeerKey" :required="true" autocomplete="one-time-code" placeholder="PDUxxxx"></HiddenInput> <HiddenInput id="pushdeer-key" v-model="$parent.notification.pushdeerKey" :required="true" autocomplete="new-password" placeholder="PDUxxxx"></HiddenInput>
</div> </div>
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;"> <i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="mb-3"> <div class="mb-3">
<label for="pushbullet-access-token" class="form-label">{{ $t("Access Token") }}</label> <label for="pushbullet-access-token" class="form-label">{{ $t("Access Token") }}</label>
<HiddenInput id="pushbullet-access-token" v-model="$parent.notification.pushbulletAccessToken" :required="true" autocomplete="one-time-code"></HiddenInput> <HiddenInput id="pushbullet-access-token" v-model="$parent.notification.pushbulletAccessToken" :required="true" autocomplete="new-password"></HiddenInput>
</div> </div>
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;"> <i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">

View file

@ -1,9 +1,9 @@
<template> <template>
<div class="mb-3"> <div class="mb-3">
<label for="pushover-user" class="form-label">{{ $t("User Key") }}<span style="color: red;"><sup>*</sup></span></label> <label for="pushover-user" class="form-label">{{ $t("User Key") }}<span style="color: red;"><sup>*</sup></span></label>
<HiddenInput id="pushover-user" v-model="$parent.notification.pushoveruserkey" :required="true" autocomplete="one-time-code"></HiddenInput> <HiddenInput id="pushover-user" v-model="$parent.notification.pushoveruserkey" :required="true" autocomplete="new-password"></HiddenInput>
<label for="pushover-app-token" class="form-label">{{ $t("Application Token") }}<span style="color: red;"><sup>*</sup></span></label> <label for="pushover-app-token" class="form-label">{{ $t("Application Token") }}<span style="color: red;"><sup>*</sup></span></label>
<HiddenInput id="pushover-app-token" v-model="$parent.notification.pushoverapptoken" :required="true" autocomplete="one-time-code"></HiddenInput> <HiddenInput id="pushover-app-token" v-model="$parent.notification.pushoverapptoken" :required="true" autocomplete="new-password"></HiddenInput>
<label for="pushover-device" class="form-label">{{ $t("Device") }}</label> <label for="pushover-device" class="form-label">{{ $t("Device") }}</label>
<input id="pushover-device" v-model="$parent.notification.pushoverdevice" type="text" class="form-control"> <input id="pushover-device" v-model="$parent.notification.pushoverdevice" type="text" class="form-control">
<label for="pushover-device" class="form-label">{{ $t("Message Title") }}</label> <label for="pushover-device" class="form-label">{{ $t("Message Title") }}</label>

View file

@ -1,13 +1,13 @@
<template> <template>
<div class="mb-3"> <div class="mb-3">
<label for="pushy-app-token" class="form-label">{{ $t("pushyAPIKey") }}</label> <label for="pushy-app-token" class="form-label">{{ $t("pushyAPIKey") }}</label>
<HiddenInput id="pushy-app-token" v-model="$parent.notification.pushyAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput> <HiddenInput id="pushy-app-token" v-model="$parent.notification.pushyAPIKey" :required="true" autocomplete="new-password"></HiddenInput>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="pushy-user-key" class="form-label">{{ $t("pushyToken") }}</label> <label for="pushy-user-key" class="form-label">{{ $t("pushyToken") }}</label>
<div class="input-group mb-3"> <div class="input-group mb-3">
<HiddenInput id="pushy-user-key" v-model="$parent.notification.pushyToken" :required="true" autocomplete="one-time-code"></HiddenInput> <HiddenInput id="pushy-user-key" v-model="$parent.notification.pushyToken" :required="true" autocomplete="new-password"></HiddenInput>
</div> </div>
</div> </div>
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;"> <i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">

View file

@ -0,0 +1,40 @@
<template>
<div class="mb-3">
<label for="smseagle-url" class="form-label">{{ $t("smseagleUrl") }}</label>
<input id="smseagle-url" v-model="$parent.notification.smseagleUrl" type="text" minlength="7" class="form-control" placeholder="http://127.0.0.1" required>
</div>
<div class="mb-3">
<label for="smseagle-token" class="form-label">{{ $t("smseagleToken") }}</label>
<HiddenInput id="smseagle-token" v-model="$parent.notification.smseagleToken" :required="true"></HiddenInput>
</div>
<div class="mb-3">
<label for="smseagle-recipient-type" class="form-label">{{ $t("smseagleRecipientType") }}</label>
<select id="smseagle-recipient-type" v-model="$parent.notification.smseagleRecipientType" class="form-select">
<option value="smseagle-to" selected>{{ $t("smseagleTo") }}</option>
<option value="smseagle-group">{{ $t("smseagleGroup") }}</option>
<option value="smseagle-contact">{{ $t("smseagleContact") }}</option>
</select>
</div>
<div class="mb-3">
<label for="smseagle-recipient" class="form-label">{{ $t("smseagleRecipient") }}</label>
<input id="smseagle-recipient" v-model="$parent.notification.smseagleRecipient" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="smseagle-priority" class="form-label">{{ $t("smseaglePriority") }}</label>
<input id="smseagle-priority" v-model="$parent.notification.smseaglePriority" type="number" class="form-control" min="0" max="9" step="1" placeholder="0">
</div>
<div class="mb-3 form-check form-switch">
<label for="smseagle-encoding" class="form-label">{{ $t("smseagleEncoding") }}</label>
<input id="smseagle-encoding" v-model="$parent.notification.smseagleEncoding" type="checkbox" class="form-check-input">
</div>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
};
</script>

View file

@ -2,7 +2,7 @@
<div class="mb-3"> <div class="mb-3">
<label for="smsmanager-key" class="form-label">API Key</label> <label for="smsmanager-key" class="form-label">API Key</label>
<div class="form-text"> <div class="form-text">
{{ $t("SMSManager API Docs ") }} {{ $t("SMSManager API Docs") }}
<a href="https://smsmanager.cz/api/http#send" target="_blank">{{ $t("here") }}</a> <a href="https://smsmanager.cz/api/http#send" target="_blank">{{ $t("here") }}</a>
</div> </div>
<input id="smsmanager-key" v-model="$parent.notification.smsmanagerApiKey" type="text" class="form-control"> <input id="smsmanager-key" v-model="$parent.notification.smsmanagerApiKey" type="text" class="form-control">

View file

@ -34,7 +34,7 @@
<div class="mb-3"> <div class="mb-3">
<label for="password" class="form-label">{{ $t("Password") }}</label> <label for="password" class="form-label">{{ $t("Password") }}</label>
<HiddenInput id="password" v-model="$parent.notification.smtpPassword" :required="false" autocomplete="one-time-code"></HiddenInput> <HiddenInput id="password" v-model="$parent.notification.smtpPassword" :required="false" autocomplete="new-password"></HiddenInput>
</div> </div>
<div class="mb-3"> <div class="mb-3">

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="mb-3"> <div class="mb-3">
<label for="serverchan-sendkey" class="form-label">{{ $t("SendKey") }}</label> <label for="serverchan-sendkey" class="form-label">{{ $t("SendKey") }}</label>
<HiddenInput id="serverchan-sendkey" v-model="$parent.notification.serverChanSendKey" :required="true" autocomplete="one-time-code"></HiddenInput> <HiddenInput id="serverchan-sendkey" v-model="$parent.notification.serverChanSendKey" :required="true" autocomplete="new-password"></HiddenInput>
</div> </div>
</template> </template>

View file

@ -5,7 +5,7 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="serwersms-key" class="form-label">{{ $t('serwersmsAPIPassword') }}</label> <label for="serwersms-key" class="form-label">{{ $t('serwersmsAPIPassword') }}</label>
<HiddenInput id="serwersms-key" v-model="$parent.notification.serwersmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput> <HiddenInput id="serwersms-key" v-model="$parent.notification.serwersmsPassword" :required="true" autocomplete="new-password"></HiddenInput>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="serwersms-phone-number" class="form-label">{{ $t("serwersmsPhoneNumber") }}</label> <label for="serwersms-phone-number" class="form-label">{{ $t("serwersmsPhoneNumber") }}</label>

View file

@ -0,0 +1,6 @@
<template>
<div class="mb-3">
<label for="webhook-url" class="form-label">{{ $t("Post URL") }}</label>
<input id="webhook-url" v-model="$parent.notification.squadcastWebhookURL" type="url" pattern="https?://.+" class="form-control" required>
</div>
</template>

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="mb-3"> <div class="mb-3">
<label for="push-api-key" class="form-label">{{ $t("API Key") }}</label> <label for="push-api-key" class="form-label">{{ $t("API Key") }}</label>
<HiddenInput id="push-api-key" v-model="$parent.notification.pushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput> <HiddenInput id="push-api-key" v-model="$parent.notification.pushAPIKey" :required="true" autocomplete="new-password"></HiddenInput>
</div> </div>
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;"> <i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="mb-3"> <div class="mb-3">
<label for="telegram-bot-token" class="form-label">{{ $t("Bot Token") }}</label> <label for="telegram-bot-token" class="form-label">{{ $t("Bot Token") }}</label>
<HiddenInput id="telegram-bot-token" v-model="$parent.notification.telegramBotToken" :required="true" autocomplete="one-time-code"></HiddenInput> <HiddenInput id="telegram-bot-token" v-model="$parent.notification.telegramBotToken" :required="true" autocomplete="new-password"></HiddenInput>
<i18n-t tag="div" keypath="wayToGetTelegramToken" class="form-text"> <i18n-t tag="div" keypath="wayToGetTelegramToken" class="form-text">
<a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a> <a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a>
</i18n-t> </i18n-t>

View file

@ -7,6 +7,7 @@ import ClickSendSMS from "./ClickSendSMS.vue";
import DingDing from "./DingDing.vue"; import DingDing from "./DingDing.vue";
import Discord from "./Discord.vue"; import Discord from "./Discord.vue";
import Feishu from "./Feishu.vue"; import Feishu from "./Feishu.vue";
import FreeMobile from "./FreeMobile.vue";
import GoogleChat from "./GoogleChat.vue"; import GoogleChat from "./GoogleChat.vue";
import Gorush from "./Gorush.vue"; import Gorush from "./Gorush.vue";
import Gotify from "./Gotify.vue"; import Gotify from "./Gotify.vue";
@ -31,6 +32,8 @@ import SerwerSMS from "./SerwerSMS.vue";
import Signal from "./Signal.vue"; import Signal from "./Signal.vue";
import SMSManager from "./SMSManager.vue"; import SMSManager from "./SMSManager.vue";
import Slack from "./Slack.vue"; import Slack from "./Slack.vue";
import Squadcast from "./Squadcast.vue";
import SMSEagle from "./SMSEagle.vue";
import Stackfield from "./Stackfield.vue"; import Stackfield from "./Stackfield.vue";
import STMP from "./SMTP.vue"; import STMP from "./SMTP.vue";
import Teams from "./Teams.vue"; import Teams from "./Teams.vue";
@ -55,6 +58,7 @@ const NotificationFormList = {
"DingDing": DingDing, "DingDing": DingDing,
"discord": Discord, "discord": Discord,
"Feishu": Feishu, "Feishu": Feishu,
"FreeMobile": FreeMobile,
"GoogleChat": GoogleChat, "GoogleChat": GoogleChat,
"gorush": Gorush, "gorush": Gorush,
"gotify": Gotify, "gotify": Gotify,
@ -79,6 +83,8 @@ const NotificationFormList = {
"signal": Signal, "signal": Signal,
"SMSManager": SMSManager, "SMSManager": SMSManager,
"slack": Slack, "slack": Slack,
"squadcast": Squadcast,
"SMSEagle": SMSEagle,
"smtp": STMP, "smtp": STMP,
"stackfield": Stackfield, "stackfield": Stackfield,
"teams": Teams, "teams": Teams,

View file

@ -1,10 +1,10 @@
<template> <template>
<div> <div>
<form class="my-4" @submit.prevent="saveGeneral"> <form class="my-4" autocomplete="off" @submit.prevent="saveGeneral">
<!-- Timezone --> <!-- Client side Timezone -->
<div class="mb-4"> <div class="mb-4">
<label for="timezone" class="form-label"> <label for="timezone" class="form-label">
{{ $t("Timezone") }} {{ $t("Display Timezone") }}
</label> </label>
<select id="timezone" v-model="$root.userTimezone" class="form-select"> <select id="timezone" v-model="$root.userTimezone" class="form-select">
<option value="auto"> <option value="auto">
@ -20,6 +20,23 @@
</select> </select>
</div> </div>
<!-- Server Timezone -->
<div class="mb-4">
<label for="timezone" class="form-label">
{{ $t("Server Timezone") }}
</label>
<select id="timezone" v-model="settings.serverTimezone" class="form-select">
<option value="UTC">UTC</option>
<option
v-for="(timezone, index) in timezoneList"
:key="index"
:value="timezone.value"
>
{{ timezone.name }}
</option>
</select>
</div>
<!-- Search Engine --> <!-- Search Engine -->
<div class="mb-4"> <div class="mb-4">
<label class="form-label"> <label class="form-label">
@ -105,6 +122,7 @@
name="primaryBaseURL" name="primaryBaseURL"
placeholder="https://" placeholder="https://"
pattern="https?://.+" pattern="https?://.+"
autocomplete="new-password"
/> />
<button class="btn btn-outline-primary" type="button" @click="autoGetPrimaryBaseURL"> <button class="btn btn-outline-primary" type="button" @click="autoGetPrimaryBaseURL">
{{ $t("Auto Get") }} {{ $t("Auto Get") }}
@ -122,7 +140,7 @@
<HiddenInput <HiddenInput
id="steamAPIKey" id="steamAPIKey"
v-model="settings.steamAPIKey" v-model="settings.steamAPIKey"
autocomplete="one-time-code" autocomplete="new-password"
/> />
<div class="form-text"> <div class="form-text">
{{ $t("steamApiKeyDescription") }} {{ $t("steamApiKeyDescription") }}
@ -145,11 +163,7 @@
<script> <script>
import HiddenInput from "../../components/HiddenInput.vue"; import HiddenInput from "../../components/HiddenInput.vue";
import dayjs from "dayjs"; import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import { timezoneList } from "../../util-frontend"; import { timezoneList } from "../../util-frontend";
dayjs.extend(utc);
dayjs.extend(timezone);
export default { export default {
components: { components: {

View file

@ -41,7 +41,7 @@
<HiddenInput <HiddenInput
id="cloudflareTunnelToken" id="cloudflareTunnelToken"
v-model="cloudflareTunnelToken" v-model="cloudflareTunnelToken"
autocomplete="one-time-code" autocomplete="new-password"
:readonly="running" :readonly="running"
/> />
<div class="form-text"> <div class="form-text">

View file

@ -1,4 +1,4 @@
import { createI18n } from "vue-i18n/index"; import { createI18n } from "vue-i18n/dist/vue-i18n.esm-browser.prod.js";
import en from "./languages/en"; import en from "./languages/en";
const languageList = { const languageList = {
@ -34,6 +34,7 @@ const languageList = {
"zh-TW": "繁體中文 (台灣)", "zh-TW": "繁體中文 (台灣)",
"uk-UA": "Український", "uk-UA": "Український",
"th-TH": "ไทย", "th-TH": "ไทย",
"el-GR": "Ελληνικά",
}; };
let messages = { let messages = {

View file

@ -41,6 +41,9 @@ import {
faUndo, faUndo,
faPlusCircle, faPlusCircle,
faAngleDown, faAngleDown,
faWrench,
faHeartbeat,
faFilter,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
library.add( library.add(
@ -82,6 +85,9 @@ library.add(
faPlusCircle, faPlusCircle,
faAngleDown, faAngleDown,
faLink, faLink,
faWrench,
faHeartbeat,
faFilter,
); );
export { FontAwesomeIcon }; export { FontAwesomeIcon };

View file

@ -1,8 +1,12 @@
# How to translate # How to translate
1. Fork this repo. 1. Fork this repo.
2. Create a language file (e.g. `zh-TW.js`). The filename must be ISO language code: http://www.lingoes.net/en/translator/langcode.htm 2. Run `npm install`
3. Run `npm run update-language-files`. You can also use this command to check if there are new strings to translate for your language. 3. Run `npm run update-language-files --language=<code>` where `<code>`
is a valid ISO language code:
http://www.lingoes.net/en/translator/langcode.htm. You can also use
this command to check if there are new strings to
translate for your language.
4. Your language file should be filled in. You can translate now. 4. Your language file should be filled in. You can translate now.
5. Add it into `languageList` constant. 5. Add it into `languageList` constant.
6. Make a [pull request](https://github.com/louislam/uptime-kuma/pulls) when you have done. 6. Make a [pull request](https://github.com/louislam/uptime-kuma/pulls) when you have done.

View file

@ -562,4 +562,73 @@ export default {
"Docker Host": "Docker хост", "Docker Host": "Docker хост",
"Docker Hosts": "Docker хостове", "Docker Hosts": "Docker хостове",
trustProxyDescription: "Trust 'X-Forwarded-*' headers. Ако искате да получавате правилния IP адрес на клиента, а Uptime Kuma е зад системи като Nginx или Apache, трябва да разрешите тази опция.", trustProxyDescription: "Trust 'X-Forwarded-*' headers. Ако искате да получавате правилния IP адрес на клиента, а Uptime Kuma е зад системи като Nginx или Apache, трябва да разрешите тази опция.",
Examples: "Примери",
"Home Assistant URL": "Home Assistant URL адрес",
"Long-Lived Access Token": "Long-Lived Access Token",
"Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "Long-Lived Access Token можете да създадете, като кликнете върху името на профила си (долу ляво) и превъртите до най-долу, след това кликнете върху Създаване на токен. ",
"Notification Service": "Услуга за известяване",
"default: notify all devices": "по подразбиране: извести всички устройства",
"A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.": "Списък с услугите за известяване може да бъде намерен в Home Assistant под \"Developer Tools > Services\", там потърсете \"notification\", за да намерите името на вашето устройство/телефон.",
"Automations can optionally be triggered in Home Assistant:": "Автоматизациите могат да се задействат при нужда в Home Assistant:",
"Trigger type:": "Задействане тип:",
"Event type:": "Събитие тип:",
"Event data:": "Събитие данни:",
"Then choose an action, for example switch the scene to where an RGB light is red.": "След което изберете действие, например да превключите сцената, където RGB светлината е червена.",
"Frontend Version": "Фронтенд версия",
"Frontend Version do not match backend version!": "Фронтенд версията не съвпада с Бекенд версията!",
"Base URL": "Базов URL адрес",
goAlertInfo: "GoAlert е приложение с отворен код за планиране на повиквания, автоматизирани ескалации и известия (като SMS или гласови повиквания). Автоматично ангажирайте точния човек, по точния начин и в точното време! {0}",
goAlertIntegrationKeyInfo: "Вземете общ API интеграционен ключ за услугата във формат \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" обикновено стойността на параметъра token на копирания URL адрес.",
goAlert: "GoAlert",
backupOutdatedWarning: "Отпаднало: Тъй като са добавени много функции, тази опция за архивиране не е достатъчно поддържана и не може да генерира или възстанови пълен архив.",
backupRecommend: "Моля, архивирайте дяла или папката (./data/) директно вместо това.",
Maintenance: "Поддръжка",
statusMaintenance: "Поддръжка",
"Schedule maintenance": "Планиране на поддръжка",
"Affected Monitors": "Засегнати монитори",
"Pick Affected Monitors...": "Изберете засегнати монитори...",
"Start of maintenance": "Стартирай поддръжка",
"All Status Pages": "Всички статус страници",
"Select status pages...": "Изберете статус страници...",
recurringIntervalMessage: "Изпълнявай ежедневно | Изпълнявай всеки {0} дни",
affectedMonitorsDescription: "Изберете монитори, засегнати от текущата поддръжка",
affectedStatusPages: "Покажи това съобщение за поддръжка на избрани статус страници",
atLeastOneMonitor: "Изберете поне един засегнат монитор",
deleteMaintenanceMsg: "Сигурни ли сте, че желаете да изтриете тази поддръжка?",
Optional: "По желание",
squadcast: "Squadcast",
SendKey: "SendKey",
"SMSManager API Docs": "SMSManager API Документация ",
"Gateway Type": "Тип на шлюза",
SMSManager: "SMSManager",
"You can divide numbers with": "Може да разделяте числата с",
or: "или",
recurringInterval: "Интервал",
Recurring: "Повтаряне",
strategyManual: "Активен/Неактивен ръчно",
warningTimezone: "Използва се часовата зона на сървъра",
weekdayShortMon: "Пон",
weekdayShortTue: "Вт",
weekdayShortWed: "Ср",
weekdayShortThu: "Чет",
weekdayShortFri: "Пет",
weekdayShortSat: "Съб",
weekdayShortSun: "Нед",
dayOfWeek: "Ден",
dayOfMonth: "Дата",
lastDay: "Последен ден",
lastDay1: "Последен ден от месеца",
lastDay2: "2-ри последен ден на месеца",
lastDay3: "3-ти последен ден на месеца",
lastDay4: "4-ти последен ден на месеца",
"No Maintenance": "Няма поддръжка",
pauseMaintenanceMsg: "Сигурни ли сте, че желаете да направите пауза?",
"maintenanceStatus-under-maintenance": "В режим подръжка",
"maintenanceStatus-inactive": "Неактивен",
"maintenanceStatus-scheduled": "Планиран",
"maintenanceStatus-ended": "Прилючена",
"maintenanceStatus-unknown": "Неизвестен",
"Display Timezone": "Покажи часова зона",
"Server Timezone": "Часова зона на сървъра",
statusPageMaintenanceEndDate: "Край",
}; };

View file

@ -68,7 +68,7 @@ export default {
Timezone: "Zeitzone", Timezone: "Zeitzone",
"Search Engine Visibility": "Sichtbarkeit für Suchmaschinen", "Search Engine Visibility": "Sichtbarkeit für Suchmaschinen",
"Allow indexing": "Indizierung zulassen", "Allow indexing": "Indizierung zulassen",
"Discourage search engines from indexing site": "Halte Suchmaschinen von der Indexierung der Seite ab", "Discourage search engines from indexing site": "Suchmaschinen darum bitten, die Seite nicht zu indizieren",
"Change Password": "Passwort ändern", "Change Password": "Passwort ändern",
"Current Password": "Aktuelles Passwort", "Current Password": "Aktuelles Passwort",
"New Password": "Neues Passwort", "New Password": "Neues Passwort",
@ -78,7 +78,7 @@ export default {
"Disable Auth": "Authentifizierung deaktivieren", "Disable Auth": "Authentifizierung deaktivieren",
"Enable Auth": "Authentifizierung aktivieren", "Enable Auth": "Authentifizierung aktivieren",
"disableauth.message1": "Bist du sicher das du die <strong>Authentifizierung deaktivieren</strong> möchtest?", "disableauth.message1": "Bist du sicher das du die <strong>Authentifizierung deaktivieren</strong> möchtest?",
"disableauth.message2": "Es ist für <strong>jemanden der eine externe Authentifizierung</strong> vor Uptime Kuma geschaltet hat, wie z.B. Cloudflare Access.", "disableauth.message2": "Dies ist für Szenarien gedacht, <strong>in denen man eine externe Authentifizierung</strong> vor Uptime Kuma geschaltet hat, wie z.B. Cloudflare Access, Authelia oder andere Authentifizierungsmechanismen.",
"Please use this option carefully!": "Bitte mit Vorsicht nutzen.", "Please use this option carefully!": "Bitte mit Vorsicht nutzen.",
Logout: "Ausloggen", Logout: "Ausloggen",
notificationDescription: "Benachrichtigungen müssen einem Monitor zugewiesen werden, damit diese funktionieren.", notificationDescription: "Benachrichtigungen müssen einem Monitor zugewiesen werden, damit diese funktionieren.",
@ -559,7 +559,7 @@ export default {
"ntfy Topic": "ntfy Thema", "ntfy Topic": "ntfy Thema",
Domain: "Domain", Domain: "Domain",
Workstation: "Workstation", Workstation: "Workstation",
disableCloudflaredNoAuthMsg: "Du bist im nicht-authentifizieren modus, ein Passwort wird nicht benötigt.", disableCloudflaredNoAuthMsg: "Du bist im nicht-authentifizieren Modus, ein Passwort wird nicht benötigt.",
trustProxyDescription: "Vertraue 'X-Forwarded-*' headern. Wenn man die richtige client IP haben möchte und Uptime Kuma hinter einem Proxy wie Nginx or Apache läuft, wollte dies aktiviert werden.", trustProxyDescription: "Vertraue 'X-Forwarded-*' headern. Wenn man die richtige client IP haben möchte und Uptime Kuma hinter einem Proxy wie Nginx or Apache läuft, wollte dies aktiviert werden.",
wayToGetLineNotifyToken: "Du kannst hier ein Token erhalten: {0}", wayToGetLineNotifyToken: "Du kannst hier ein Token erhalten: {0}",
Examples: "Beispiele", Examples: "Beispiele",
@ -576,4 +576,59 @@ export default {
"Then choose an action, for example switch the scene to where an RGB light is red.": "Dann eine Aktion wählen, zum Beispiel eine Scene wählen in der ein RGB Licht rot ist.", "Then choose an action, for example switch the scene to where an RGB light is red.": "Dann eine Aktion wählen, zum Beispiel eine Scene wählen in der ein RGB Licht rot ist.",
"Frontend Version": "Frontend Version", "Frontend Version": "Frontend Version",
"Frontend Version do not match backend version!": "Die Frontend Version stimmt nicht mit der backend version überein!", "Frontend Version do not match backend version!": "Die Frontend Version stimmt nicht mit der backend version überein!",
Maintenance: "Wartung",
statusMaintenance: "Wartung",
"Schedule maintenance": "Geplante Wartung",
"Affected Monitors": "Betroffene Monitore",
"Pick Affected Monitors...": "Wähle betroffene Monitore...",
"Start of maintenance": "Beginn der Wartung",
"All Status Pages": "Alle Status Seiten",
"Select status pages...": "Wähle Status Seiten...",
recurringIntervalMessage: "einmal pro Tag ausgeführt | Wird alle {0} Tage ausgführt",
affectedMonitorsDescription: "Wähle alle Monitore die von der Wartung betroffen sind",
affectedStatusPages: "Zeige diese Nachricht auf ausgewählten Status Seiten",
atLeastOneMonitor: "Wähle mindestens einen Monitor",
deleteMaintenanceMsg: "Möchtest du diese Wartung löschen?",
"Base URL": "Basis URL",
goAlertInfo: "GoAlert ist eine Open-Source Applikation für Rufbereitschaft Planung, automaitsche Esklaltion und Benachrichtigung (z.B. SMS oder Telefonanrufe). Beauftragen Sie automatisch die richtige Person, auf die richtige Art und Weise und zum richtigen Zeitpunkt! {0}",
goAlertIntegrationKeyInfo: "Bekomm einenen gernerischen API Schlüssel in folgeden Format \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\". Normalerweise der Wert des Token aus der URL.",
goAlert: "GoAlert",
backupOutdatedWarning: "Veraltet: Eine menge Neuerungen sind eingeflossen und diese Funktion wurde etwas vernachlässigt worden. Es kann kein vollständiges Backup erstellt oder eingspielt werden.",
backupRecommend: "Bitte Backup das Volume oder den Ordner (./ data /) selbst.",
Optional: "Optional",
squadcast: "Squadcast",
SendKey: "SendKey",
"SMSManager API Docs": "SMSManager API Dokumente",
"Gateway Type": "Gateway Type",
SMSManager: "SMSManager",
"You can divide numbers with": "Du kannst Zahlen teilen mit",
or: "oder",
recurringInterval: "Intervall",
Recurring: "Wiederkehrend",
strategyManual: "Active/Inactive Manually",
warningTimezone: "Es wird die Zeitzone des Servers genutzt",
weekdayShortMon: "Mo",
weekdayShortTue: "Di",
weekdayShortWed: "Mi",
weekdayShortThu: "Do",
weekdayShortFri: "Fr",
weekdayShortSat: "Sa",
weekdayShortSun: "So",
dayOfWeek: "Tag der Woche",
dayOfMonth: "Tag im Monat",
lastDay: "Letzter Tag",
lastDay1: "Letzter Tag im Monat",
lastDay2: "Vorletzer Tag im Monat",
lastDay3: "3. letzter Tag im Monat",
lastDay4: "4. letzter Tag im Monat",
"No Maintenance": "Keine Wartung",
pauseMaintenanceMsg: "Möchtest du wirklich pausieren?",
"maintenanceStatus-under-maintenance": "Unter Wartung",
"maintenanceStatus-inactive": "Inaktiv",
"maintenanceStatus-scheduled": "Geplant",
"maintenanceStatus-ended": "Ende",
"maintenanceStatus-unknown": "Unbekannt",
"Display Timezone": "Zeitzone anzeigen",
"Server Timezone": "Server Zeitzone",
statusPageMaintenanceEndDate: "Ende",
}; };

585
src/languages/el-GR.js Normal file
View file

@ -0,0 +1,585 @@
export default {
languageName: "Ελληνικά",
checkEverySecond: "Έλεγχος κάθε {0} δευτερόλεπτα",
retryCheckEverySecond: "Επανάληψη κάθε {0} δευτερόλεπτα",
resendEveryXTimes: "Επανάληψη αποστολής ειδοποίησης κάθε {0} φορές",
resendDisabled: "Η επανάληψη αποστολής ειδοποίησης είναι απενεργοποιημένη",
retriesDescription: "Μέγιστες επαναλήψεις προτού η υπηρεσία επισημανθεί ως κατω και σταλεί μια ειδοποίηση",
ignoreTLSError: "Παράβλεψη σφάλματος TLS/SSL για ιστότοπους HTTPS",
upsideDownModeDescription: "Αναποδογυρίστε την κατάσταση. Εάν η υπηρεσία είναι προσβάσιμη, είναι ΚΑΤΩ.",
maxRedirectDescription: "Μέγιστος αριθμός redirect που θα ακολουθήσουν. Ρυθμίστε το 0 για να απενεργοποιήσετε τα redirect.",
acceptedStatusCodesDescription: "Επιλέξτε κωδικούς κατάστασης που θεωρούνται επιτυχή.",
passwordNotMatchMsg: "Ο κωδικός δεν ταιριάζει.",
notificationDescription: "Οι ειδοποιήσεις πρέπει να εκχωρηθούν σε μια παρακολούθηση για να λειτουργήσουν.",
keywordDescription: "Αναζήτηση λέξης-κλειδιού σε απλή απόκριση HTML ή JSON. Η αναζήτηση είναι διάκριση πεζών-κεφαλαίων.",
pauseDashboardHome: "Παύση",
deleteMonitorMsg: "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτήν την παρακολούθηση;",
deleteNotificationMsg: "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτήν την ειδοποίηση για όλες τις παρακολούθησης?",
dnsPortDescription: "Θύρα διακομιστή DNS. Προεπιλογή σε 53. Μπορείτε να αλλάξετε τη θύρα ανά πάσα στιγμή.",
resolverserverDescription: "Το Cloudflare είναι ο προεπιλεγμένος διακομιστής. Μπορείτε να αλλάξετε τον διακομιστή επίλυσης ανά πάσα στιγμήhe default server. You can change the resolver server anytime.",
rrtypeDescription: "Επιλέξτε τον τύπο RR που θέλετε να παρακολουθήσετε",
pauseMonitorMsg: "Είστε βέβαιοι ότι θέλετε να κάνετε παύση;",
enableDefaultNotificationDescription: "Αυτή η ειδοποίηση θα είναι ενεργοποιημένη από προεπιλογή για νέες παρακολούθησης. Μπορείτε ακόμα να απενεργοποιήσετε την ειδοποίηση ξεχωριστά για κάθε παρακολούθηση.",
clearEventsMsg: "Είστε βέβαιοι ότι θέλετε να διαγράψετε όλα τα συμβάντα για αυτήν την παρακολούθηση;",
clearHeartbeatsMsg: "Είστε βέβαιοι ότι θέλετε να διαγράψετε όλους τους καρδιακούς παλμούς για αυτήν την παρακολούθηση;",
confirmClearStatisticsMsg: "Είστε βέβαιοι ότι θέλετε να διαγράψετε ΟΛΑ τα στατιστικά στοιχεία;?",
importHandleDescription: "Επιλέξτε «Παράλειψη υπάρχοντος» εάν θέλετε να παραλείψετε κάθε παρακολούθηση ή ειδοποίηση με το ίδιο όνομα. Το 'Overwrite' θα διαγράψει κάθε υπάρχουσα παρακολούθηση και ειδοποίηση.",
confirmImportMsg: "Είστε βέβαιοι ότι θέλετε να εισαγάγετε το αντίγραφο ασφαλείας; Επαληθεύστε ότι έχετε επιλέξει τη σωστή επιλογή.",
twoFAVerifyLabel: "Εισαγάγετε το 2FA κωδικό για να επαληθεύσετε: ",
tokenValidSettingsMsg: "Ο κωδικός 2FA είναι έγκυρο! Τώρα μπορείτε να αποθηκεύσετε τις ρυθμίσεις 2FA",
confirmEnableTwoFAMsg: "Είστε βέβαιοι ότι θέλετε να ενεργοποιήσετε το 2FA;",
confirmDisableTwoFAMsg: "Είστε βέβαιοι ότι θέλετε να απενεργοποιήσετε το 2FA;",
Settings: "Ρυθμίσεις",
Dashboard: "Πίνακας",
"New Update": "Νέα αναβάθμιση",
Language: "Γλώσσα",
Appearance: "Εμφάνιση",
Theme: "Θέμα",
General: "Γενικά",
"Primary Base URL": "Κύρια βασική διεύθυνση URL",
Version: "Εκδοχή",
"Check Update On GitHub": "Ελέγξτε για Ενημέρωση στο GitHub",
List: "Λίστα",
Add: "Προσθήκη",
"Add New Monitor": "Προσθήκη νέας παρακολούθησης",
"Quick Stats": "Γρήγορα στατιστικά",
Up: "Πάνω",
Down: "Κάτω",
Pending: "Εκκρεμεί",
Unknown: "Άγνωστο",
Pause: "Παύση",
Name: "Ονομα",
Status: "Κατάσταση",
DateTime: "ΗμερομηνίαΏρα",
Message: "Μήνυμα",
"No important events": "Δεν υπάρχουν σημαντικά γεγονότα",
Resume: "Συνέχιση",
Edit: "Επεξεργασία",
Delete: "Διαγράφη",
Current: "Current",
Uptime: "Χρόνος λειτουργίας",
"Cert Exp.": "Cert Exp.",
day: "ημέρα | ημέρες",
"-day": "-ημέρα",
hour: "ώρα",
"-hour": "-ώρα",
Response: "Απάντηση",
Ping: "Ping",
"Monitor Type": "Τύπος παρακολούθησης",
Keyword: "Λέξη-κλειδί",
"Friendly Name": "Φιλικό όνομα",
URL: "URL",
Hostname: "Hostname",
Port: "Port",
"Heartbeat Interval": "Διάστημα καρδιακών παλμών",
Retries: "Επαναλήψεις",
"Heartbeat Retry Interval": "Διάστημα επανάληψης παλμών καρδιάς",
"Resend Notification if Down X times consequently": "Αποστολή νέας ειδοποίησης εάν κατω X φορές κατά συνέχεια",
Advanced: "Προχωρημένα",
"Upside Down Mode": "Ανάποδη λειτουργία",
"Max. Redirects": "Μέγιστη. Ανακατευθύνσεις",
"Accepted Status Codes": "Αποδεκτοί Κωδικοί Κατάστασης",
"Push URL": "Push URL",
needPushEvery: "Θα πρέπει να καλείτε αυτήν τη διεύθυνση URL κάθε {0} δευτερόλεπτα.",
pushOptionalParams: "Προαιρετικές παράμετροι: {0}",
Save: "Αποθηκεύση",
Notifications: "Ειδοποιήσεις",
"Not available, please setup.": "Μη διαθέσιμο, παρακαλώ ρυθμίστε.",
"Setup Notification": "Δημιουργία ειδοποίησης",
Light: "Φωτεινό",
Dark: "Σκοτεινό",
Auto: "Αυτόματο",
"Theme - Heartbeat Bar": "Θέμα - Μπάρα καρδιακών παλμών",
Normal: "Κανονικό",
Bottom: "Κάτω μέρος",
None: "Τίποτα",
Timezone: "Ζώνη ώρας",
"Search Engine Visibility": "Ορατότητα μηχανών αναζήτησης",
"Allow indexing": "Να επιτρέπεται η ευρετηρίαση",
"Discourage search engines from indexing site": "Αποθαρρύνετε τις μηχανές αναζήτησης από την ευρετηρίαση ιστότοπου",
"Change Password": "Αλλαγή κωδικού πρόσβασης",
"Current Password": "Τρέχων κωδικός πρόσβασης",
"New Password": "Νέος κωδικός πρόσβασης",
"Repeat New Password": "Επαναλάβετε τον νέο κωδικό πρόσβασης",
"Update Password": "Ενημέρωση κωδικού πρόσβασης",
"Disable Auth": "Απενεργοποίηση ελέγχου ταυτότητας",
"Enable Auth": "Ενεργοποίηση ελέγχου ταυτότητας",
"disableauth.message1": "Είστε βέβαιοι ότι θέλετε να <strong>απενεργοποιήσετε τον έλεγχο ταυτότητας</strong>;",
"disableauth.message2": "Έχει σχεδιαστεί για σενάρια <strong>όπου σκοπεύετε να εφαρμόσετε έλεγχο ταυτότητας τρίτου μέρους</strong> μπροστά από το Uptime Kuma, όπως το Cloudflare Access, Authelia ή άλλους μηχανισμούς ελέγχου ταυτότητας.",
"Please use this option carefully!": "Χρησιμοποιήστε αυτή την επιλογή προσεκτικά!",
Logout: "Αποσύνδεση",
Leave: "Φύγετε",
"I understand, please disable": "Καταλαβαίνω, απενεργοποιήστε",
Confirm: "Επιβεβαίωση",
Yes: "Ναί",
No: "Οχι",
Username: "Όνομα χρήστη",
Password: "Κωδικός πρόσβασης",
"Remember me": "Θυμήσου με",
Login: "Σύνδεση",
"No Monitors, please": "Δεν υπάρχουν παρακολούθησης παρακαλώ",
"add one": "προσθέστε ένα",
"Notification Type": "Είδος ειδοποίησης",
Email: "Email",
Test: "Δοκιμή",
"Certificate Info": "Πληροφορίες πιστοποιητικού",
"Resolver Server": "Διακομιστής επίλυσης",
"Resource Record Type": "Τύπος εγγραφής πόρων",
"Last Result": "Τελευταίο Αποτέλεσμα",
"Create your admin account": "Δημιουργήστε τον λογαριασμό διαχειριστή σας",
"Repeat Password": "Επαναλάβετε τον κωδικό πρόσβασης",
"Import Backup": "Εισαγωγή αντιγράφων ασφαλείας",
"Export Backup": "Εξαγωγή αντιγράφων ασφαλείας",
Export: "Εξαγωγή",
Import: "Εισαγωγή",
respTime: "Χρόν. Aπό (ms)",
notAvailableShort: "N/A",
"Default enabled": "Προεπιλογή ενεργοποιημένη",
"Apply on all existing monitors": "Εφαρμόστε σε όλες τις υπάρχουσες παρακολούθησης",
Create: "Δημιουργία",
"Clear Data": "Καθαρισμός δεδομένων",
Events: "Γεγονότα",
Heartbeats: "Παλμοι καρδιας",
"Auto Get": "Αυτόματη λήψη",
backupDescription: "Μπορείτε να δημιουργήσετε αντίγραφα ασφαλείας γία ολλες της παρακολούθησης και ειδοποιήσης σε ένα αρχείο JSON.",
backupDescription2: "Σημείωση: δεν περιλαμβάνονται δεδομένα ιστορικού και συμβάντων.",
backupDescription3: "Στο αρχείο εξαγωγής περιλαμβάνονται ευαίσθητα δεδομένα, όπως token ειδοποιήσεων. Aποθηκεύστε την εξαγωγή με ασφάλεια.",
alertNoFile: "Επιλέξτε ένα αρχείο για εισαγωγή.",
alertWrongFileType: "Επιλέξτε ένα αρχείο JSON.",
"Clear all statistics": "Εκκαθάριση όλων των στατιστικών",
"Skip existing": "Παράβλεψη υπάρχοντος",
Overwrite: "Αντικατάσταση",
Options: "Επιλογές",
"Keep both": "Κράτα και τα δύο",
"Verify Token": "Επαλήθευση Token",
"Setup 2FA": "Ρύθμιση 2FA",
"Enable 2FA": "Ενεργοποίηση 2FA",
"Disable 2FA": "Απενεργοποίηση 2FA",
"2FA Settings": "Ρυθμίσεις 2FA",
"Two Factor Authentication": "Έλεγχος ταυτότητας δύο παραγόντων",
Active: "Ενεργός",
Inactive: "Ανενεργό",
Token: "Token",
"Show URI": "Εμφάνιση URI",
Tags: "Ετικέτες",
"Add New below or Select...": "Προσθήκη νέου παρακάτω ή Επιλέξτε...",
"Tag with this name already exist.": "Υπάρχει ήδη η ετικέτα με αυτό το όνομα.",
"Tag with this value already exist.": "Υπάρχει ήδη ετικέτα με αυτό το value.",
color: "χρώμα",
"value (optional)": "value (optional)",
Gray: "Γκρί",
Red: "Κόκκινο",
Orange: "Πορτοκάλι",
Green: "Πράσινο",
Blue: "Μπλε",
Indigo: "Indigo",
Purple: "Μωβ",
Pink: "Ροζ",
"Search...": "Αναζήτηση...",
"Avg. Ping": "Μέσo.Ping",
"Avg. Response": "Μέσo. Aπάντηση",
"Entry Page": "Σελίδα εισαγωγής",
statusPageNothing: "Δεν υπάρχει τίποτα εδώ, προσθέστε μια ομάδα ή μια παρακολούθηση.",
"No Services": "Δεν υπάρχουν υπηρεσίες",
"All Systems Operational": "Όλα τα συστήματα λειτουργούν",
"Partially Degraded Service": "Μερικώς υποβαθμισμένη υπηρεσία",
"Degraded Service": "Υποβαθμισμένη υπηρεσία",
"Add Group": "Προσθήκη γρουπ",
"Add a monitor": "Προσθήκη παρακολούθησης",
"Edit Status Page": "Επεξεργασία σελίδας κατάστασης",
"Go to Dashboard": "Μεταβείτε στον Πίνακα ελέγχου",
"Status Page": "Σελίδα κατάστασης",
"Status Pages": "Σελίδες κατάστασης",
defaultNotificationName: "Η ειδοποίηση μου {notification} ({number})",
here: "εδώ",
Required: "Απαιτείται",
telegram: "Telegram",
"Bot Token": "Διακριτικό Bot",
wayToGetTelegramToken: "Μπορείτε να πάρετε ένα διακριτικό από {0}.",
"Chat ID": "Chat ID",
supportTelegramChatID: "Support Direct Chat / Group / Channel's Chat ID",
wayToGetTelegramChatID: "Μπορείτε να λάβετε το αναγνωριστικό συνομιλίας σας στέλνοντας ένα μήνυμα στο bot και μεταβαίνοντας σε αυτήν τη διεύθυνση URL για να προβάλετε το chat_id:",
"YOUR BOT TOKEN HERE": "ΤΟ BOT ΣΑΣ ΔΙΑΚΡΙΤΙΚΌ ΕΔΩ",
chatIDNotFound: "Το Chat ID δεν βρέθηκε. Στείλτε πρώτα ένα μήνυμα σε αυτό το bot",
webhook: "Webhook",
"Post URL": "Post URL",
"Content Type": "Τύπος περιεχομένου",
webhookJsonDesc: "{0} είναι καλό για οποιονδήποτε σύγχρονο διακομιστή HTTP όπως το Express.js",
webhookFormDataDesc: "{multipart} είναι καλό για την PHP. Το JSON θα πρέπει να αναλυθεί με {decodeFunction}",
smtp: "Email (SMTP)",
secureOptionNone: "None / STARTTLS (25, 587)",
secureOptionTLS: "TLS (465)",
"Ignore TLS Error": "Παράβλεψη σφάλματος TLS",
"From Email": "Από Email",
emailCustomSubject: "Προσαρμοσμένο θέμα",
"To Email": "Προς Email",
smtpCC: "CC",
smtpBCC: "BCC",
discord: "Discord",
"Discord Webhook URL": "Discord Webhook URL",
wayToGetDiscordURL: "Μπορείτε να το αποκτήσετε μεταβαίνοντας στις Ρυθμίσεις διακομιστή -> Ενσωματώσεις -> Δημιουργία Webhook",
"Bot Display Name": "Εμφανιζόμενο όνομα bot",
"Prefix Custom Message": "Προσαρμοσμένο μήνυμα",
"Hello @everyone is...": "Γεια {'@'}everyone ειναι...",
teams: "Microsoft Teams",
"Webhook URL": "Webhook URL",
wayToGetTeamsURL: "Μπορείτε να μάθετε πώς να δημιουργείτε μια διεύθυνση URL webhook {0}.",
signal: "Signal",
Number: "Αριθμός",
Recipients: "Αποδέκτες",
needSignalAPI: "Πρέπει να έχετε ένα signal client με REST API..",
wayToCheckSignalURL: "Μπορείτε να ελέγξετε αυτό το URL για να δείτε πώς να ρυθμίσετε ένα:",
signalImportant: "ΣΗΜΑΝΤΙΚΟ: Δεν μπορείτε να συνδυάσετε ομάδες και αριθμούς στους παραλήπτες!",
gotify: "Gotify",
"Application Token": "Token εφαρμογής",
"Server URL": "URL διακομιστή",
Priority: "Προτεραιότητα",
slack: "Slack",
"Icon Emoji": "Εικονίδιο Emoji",
"Channel Name": "Όνομα καναλιού",
"Uptime Kuma URL": "Uptime Kuma URL",
aboutWebhooks: "Περισσότερες πληροφορίες σχετικά με τα Webhooks στο: {0}",
aboutChannelName: "Εισαγάγετε το όνομα του καναλιού στο {0} Όνομα καναλιού εάν θέλετε να παρακάμψετε το κανάλι Webhook. Π.χ.: #other-channel",
aboutKumaURL: "Εάν αφήσετε κενό το πεδίο URL Uptime Kuma, θα είναι προεπιλεγμένο στη σελίδα Project GitHub..",
emojiCheatSheet: "Φύλλο εξαπάτησης emoji: {0}",
"rocket.chat": "Rocket.Chat",
pushover: "Pushover",
pushy: "Pushy",
PushByTechulus: "Push by Techulus",
octopush: "Octopush",
promosms: "PromoSMS",
clicksendsms: "ClickSend SMS",
lunasea: "LunaSea",
apprise: "Apprise (Support 50+ Notification services)",
GoogleChat: "Google Chat (Google Workspace only)",
pushbullet: "Pushbullet",
line: "Line Messenger",
mattermost: "Mattermost",
"User Key": "Κλειδί χρήστη",
Device: "Συσκευή",
"Message Title": "Τίτλος μηνύματος",
"Notification Sound": "Ήχος ειδοποίησης",
"More info on:": "Περισσότερες πληροφορίες στο: {0}",
pushoverDesc1: "Η προτεραιότητα έκτακτης ανάγκης (2) έχει προεπιλεγμένο χρονικό όριο 30 δευτερολέπτων μεταξύ των επαναλήψεων και θα λήξει μετά από 1 ώρα.",
pushoverDesc2: "Εάν θέλετε να στέλνετε ειδοποιήσεις σε διαφορετικές συσκευές, συμπληρώστε το πεδίο Συσκευή.",
"SMS Type": "Τύπος SMS",
octopushTypePremium: "Premium (Γρήγορη - συνιστάται για ειδοποίηση)",
octopushTypeLowCost: "Χαμηλό κόστος (Αργό - μερικές φορές μπλοκάρεται από τον χειριστή)",
checkPrice: "Ελέγξτε τις τιμές {0}:",
apiCredentials: "API credentials",
octopushLegacyHint: "Χρησιμοποιείτε την παλαιού τύπου έκδοση του Octopush (2011-2020) ή τη νέα έκδοση;",
"Check octopush prices": "Ελέγξτε τις τιμές OctoPush {0}.",
octopushPhoneNumber: "Αριθμός τηλεφώνου (διεθνής μορφή, π.χ.: +30694345678)",
octopushSMSSender: "Όνομα αποστολέα SMS: 3-11 αλφαριθμητικοί χαρακτήρες και διάστημα (a-zA-Z0-9)",
"LunaSea Device ID": "LunaSea Device ID",
"Apprise URL": "Apprise URL",
"Example:": "Παράδειγμα: {0}",
"Read more:": "Διαβάστε περισσότερα: {0}",
"Status:": "Κατάσταση: {0}",
"Read more": "Διαβάστε περισσότερα",
appriseInstalled: "Το Apprise έχει εγκατασταθεί.",
appriseNotInstalled: "Το Apprise δεν έχει εγκατασταθεί. {0}",
"Access Token": "Access Token",
"Channel access token": "Channel Access Token",
"Line Developers Console": "Line Developers Console",
lineDevConsoleTo: "Line Developers Console - {0}",
"Basic Settings": "Βασικές ρυθμίσεις",
"User ID": "User ID",
"Messaging API": "Messaging API",
wayToGetLineChannelToken: "Πρώτα αποκτήστε πρόσβαση στο {0}, δημιουργήστε έναν πάροχο και ένα κανάλι (Messanging API) και, στη συνέχεια, μπορείτε να λάβετε το channel access token και το user ID από τα παραπάνω στοιχεία μενού.",
"Icon URL": "Διεύθυνση URL εικονιδίου",
aboutIconURL: "Μπορείτε να παρέχετε έναν σύνδεσμο προς μια εικόνα στο \"Icon URL\" για να παρακάμψετε την προεπιλεγμένη εικόνα προφίλ. Δεν θα χρησιμοποιηθεί εάν έχει οριστεί το εικονίδιο Emoji.",
aboutMattermostChannelName: "Μπορείτε να παρακάμψετε το προεπιλεγμένο κανάλι στο οποίο δημοσιεύει το Webhook εισάγοντας το όνομα του καναλιού στο πεδίο \"Όνομα καναλιού\". Αυτό πρέπει να ενεργοποιηθεί στις ρυθμίσεις του Mattermost Webhook. Π.χ.: #other-channel",
matrix: "Matrix",
promosmsTypeEco: "SMS ECO - φθηνό αλλά αργό και συχνά υπερφορτωμένο. Περιορίζεται μόνο σε Πολωνούς παραλήπτες.",
promosmsTypeFlash: "SMS FLASH - Το μήνυμα θα εμφανίζεται αυτόματα στη συσκευή του παραλήπτη. Περιορίζεται μόνο σε Πολωνούς παραλήπτες.",
promosmsTypeFull: "SMS FULL - Premium επίπεδο SMS, Μπορείτε να χρησιμοποιήσετε το Όνομα Αποστολέα σας (Πρέπει πρώτα να καταχωρήσετε το όνομα). Αξιόπιστο για ειδοποιήσεις.",
promosmsTypeSpeed: "SMS SPEED - Υψηλότερη προτεραιότητα στο σύστημα. Πολύ γρήγορο και αξιόπιστο αλλά ακριβό (περίπου διπλάσια τιμή SMS FULL).",
promosmsPhoneNumber: "Αριθμός τηλεφώνου (για πολωνούς παραλήπτες Μπορείτε να παραλείψετε τους κωδικούς περιοχής)",
promosmsSMSSender: "Όνομα αποστολέα SMS: Προεγγεγραμμένο όνομα ή ένα από τα προεπιλεγμένα: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
"Feishu WebHookUrl": "Feishu WebHookURL",
matrixHomeserverURL: "Homeserver URL (με http(s):// και προαιρετικά θύρα)",
"Internal Room Id": "Internal Room ID",
matrixDesc1: "Μπορείτε να βρείτε το internal room ID ανατρέχοντας στην ενότητα για προχωρημένους των ρυθμίσεων δωματίου στο πρόγραμμα-πελάτη Matrix. Θα πρέπει να μοιάζει με !QMdRCpUIfLwsfjxye6:home.server.",
matrixDesc2: "Συνιστάται ανεπιφύλακτα να δημιουργήσετε έναν νέο χρήστη και να μην χρησιμοποιήσετε το διακριτικό πρόσβασης του χρήστη Matrix, καθώς θα επιτρέψει την πλήρη πρόσβαση στον λογαριασμό σας και σε όλα τα δωμάτια στα οποία συμμετέχετε. Αντίθετα, δημιουργήστε έναν νέο χρήστη και προσκαλέστε τον μόνο στο δωμάτιο στο οποίο θέλετε να λαμβάνετε την ειδοποίηση. Μπορείτε να λάβετε το access token εκτελώντας {0}",
Method: "Μέθοδος",
Body: "Σώμα",
Headers: "Headers",
PushUrl: "Push URL",
HeadersInvalidFormat: "The request headers are not valid JSON: ",
BodyInvalidFormat: "The request body is not valid JSON: ",
"Monitor History": "Ιστορικο Παρακολούθησης",
clearDataOlderThan: "Διατηρήστε τα δεδομένα ιστορικού παρακολούθησης για {0} ημέρες.",
PasswordsDoNotMatch: "Οι κωδικοί πρόσβασης δεν ταιριάζουν.",
records: "εγγραφές",
"One record": "Μία εγγραφή",
steamApiKeyDescription: "Για την παρακολούθηση ενός διακομιστή παιχνιδιών Steam χρειάζεστε ένα κλειδί Steam Web-API. Μπορείτε να καταχωρήσετε το κλειδί API σας εδώ: ",
"Current User": "Τρέχων χρήστης",
topic: "Θέμα",
topicExplanation: "Θέμα MQTT προς παρακολούθηση",
successMessage: "Μήνυμα επιτυχίας",
successMessageExplanation: "Μήνυμα MQTT που θα θεωρηθεί επιτυχές",
recent: "Πρόσφατος",
Done: "Ολοκληρώθηκε",
Info: "Πληροφορίες",
Security: "Ασφάλεια",
"Steam API Key": "Steam API Key",
"Shrink Database": "Συρρίκνωση βάσης δεδομένων",
"Pick a RR-Type...": "Επιλέξτε έναν τύπο RR...",
"Pick Accepted Status Codes...": "Επιλέξτε Αποδεκτούς κωδικούς κατάστασης...",
Default: "Προκαθορισμένο",
"HTTP Options": "Επιλογές HTTP",
"Create Incident": "Δημιουργία περιστατικού",
Title: "Τίτλος",
Content: "Περιεχόμενο",
Style: "Στυλ",
info: "πληροφορίες",
warning: "προειδοποίηση",
danger: "κίνδυνος",
error: "σφάλμα",
critical: "κριτικό",
primary: "primary",
light: "light",
dark: "dark",
Post: "Δημοσίευση",
"Please input title and content": "Παρακαλούμε εισαγάγετε τίτλο και περιεχόμενο",
Created: "Δημιουργήθηκε",
"Last Updated": "Τελευταία ενημέρωση",
Unpin: "Ξεκαρφιτσώστε",
"Switch to Light Theme": "Μετάβαση σε Ανιχτό θέμα",
"Switch to Dark Theme": "Μετάβαση σε Σκούρο θέμα",
"Show Tags": "Εμφάνιση ετικετών",
"Hide Tags": "Απόκρυψη ετικετών",
Description: "Περιγραφή",
"No monitors available.": "Δεν υπάρχουν διαθέσιμες παρακολουθήσεις.",
"Add one": "Προσθέστε ένα",
"No Monitors": "Χωρίς παρακολουθήσεις",
"Untitled Group": "Ομάδα χωρίς τίτλο",
Services: "Υπηρεσίες",
Discard: "Απορρίψει",
Cancel: "Ακυρο",
"Powered by": "Με την υποστήριξη του",
shrinkDatabaseDescription: "Ενεργοποίηση βάσης δεδομένων VACUUM για SQLite. Εάν η βάση δεδομένων σας έχει δημιουργηθεί μετά την έκδοση 1.10.0, το AUTO_VACUUM είναι ήδη ενεργοποιημένο και αυτή η ενέργεια δεν χρειάζεται.",
serwersms: "SerwerSMS.pl",
serwersmsAPIUser: "API Username (incl. webapi_ prefix)",
serwersmsAPIPassword: "API κωδικός πρόσβασης",
serwersmsPhoneNumber: "Αριθμός τηλεφώνου",
serwersmsSenderName: "Όνομα αποστολέα SMS (καταχωρήθηκε μέσω της πύλης πελατών)",
stackfield: "Stackfield",
Customize: "Προσαρμογή",
"Custom Footer": "Προσαρμογή Footer",
"Custom CSS": "Προσαρμογή CSS",
smtpDkimSettings: "Ρυθμίσεις DKIM",
smtpDkimDesc: "Ανατρέξτε στο Nodemailer DKIM {0} για χρήση.",
documentation: "documentation",
smtpDkimDomain: "Domain Name",
smtpDkimKeySelector: "Key Selector",
smtpDkimPrivateKey: "Private Key",
smtpDkimHashAlgo: "Hash Algorithm (Optional)",
smtpDkimheaderFieldNames: "Header Keys to sign (Optional)",
smtpDkimskipFields: "Header Keys not to sign (Optional)",
wayToGetPagerDutyKey: "Μπορείτε να το λάβετε μεταβαίνοντας στο Service -> Service Directory -> (Επιλέξτε μια υπηρεσία) -> Integrations -> Add integration. Εδώ μπορείτε να κάνετε αναζήτηση για \"Events API V2\". Περισσότερες πληροφορίες {0}",
"Integration Key": "Integration Key",
"Integration URL": "Integration URL",
"Auto resolve or acknowledged": "Αυτόματη επίλυση ή αναγνώριση",
"do nothing": "μην κάνεις τίποτα",
"auto acknowledged": "αυτόματη αναγνώριση",
"auto resolve": "αυτόματη επίλυση",
gorush: "Gorush",
alerta: "Alerta",
alertaApiEndpoint: "API Endpoint",
alertaEnvironment: "Environment",
alertaApiKey: "API Key",
alertaAlertState: "Alert State",
alertaRecoverState: "Recover State",
deleteStatusPageMsg: "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτήν τη σελίδα κατάστασης?",
Proxies: "Proxies",
default: "Προκαθορισμένο",
enabled: "Ενεργοποιημένο",
setAsDefault: "Ορίσετε ως προεπιλογή",
deleteProxyMsg: "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το proxy για όλες τις παρακολουθήσεις;",
proxyDescription: "Πρέπει να εκχωρηθούν proxies σε μια οθπαρακολουθή για να λειτουργήσουν..",
enableProxyDescription: "Το proxy δεν θα επηρεάσει τα αιτήματα της παρακολουθήσεις μέχρι να ενεργοποιηθεί. Μπορείτε να ελέγξετε την προσωρινή απενεργοποίηση του proxy από όλες τις παρακολουθήσεις βάσει κατάστασης ενεργοποίησης.",
setAsDefaultProxyDescription: "Αυτός το proxy θα είναι ενεργοποιημένο από προεπιλογή για νέες παρακολουθήσεις. Μπορείτε ακόμα να απενεργοποιήσετε το proxy ξεχωριστά για κάθε οθόνη.",
"Certificate Chain": "Certificate Chain",
Valid: "Εγκυρο",
Invalid: "Μη έγκυρο",
AccessKeyId: "AccessKey ID",
SecretAccessKey: "AccessKey Secret",
PhoneNumbers: "PhoneNumbers",
TemplateCode: "TemplateCode",
SignName: "SignName",
"Sms template must contain parameters: ": "Το πρότυπο SMS πρέπει να περιέχει παραμέτρους: ",
"Bark Endpoint": "Bark Endpoint",
"Bark Group": "Bark Ομάδα",
"Bark Sound": "Bark Ήχος",
WebHookUrl: "WebHookUrl",
SecretKey: "SecretKey",
"For safety, must use secret key": "Για ασφάλεια, πρέπει να χρησιμοποιήσετε secret key",
"Device Token": "Device Token",
Platform: "Platform",
iOS: "iOS",
Android: "Android",
Huawei: "Huawei",
High: "High",
Retry: "Ξαναδοκιμάσετε",
Topic: "Θέμα",
"WeCom Bot Key": "WeCom Bot Key",
"Setup Proxy": "Ρύθμιση Proxy",
"Proxy Protocol": "Πρωτόκολλο Proxy",
"Proxy Server": "Proxy Server",
"Proxy server has authentication": "Το Proxy διαθέτει έλεγχο ταυτότητας",
User: "Χρήστης",
Installed: "Εγκατεστημένο",
"Not installed": "Μη εγκατεστημενο",
Running: "Τρέχη",
"Not running": "Δεν τρεχη",
"Remove Token": "Κατάργηση Token",
Start: "Αρχή",
Stop: "Στάση",
"Uptime Kuma": "Uptime Kuma",
"Add New Status Page": "Προσθήκη νέας σελίδας κατάστασης",
Slug: "Slug",
"Accept characters:": "Αποδοχή χαρακτήρων:",
startOrEndWithOnly: "Ξεκινήστε ή τελειώστε μόνο με {0}",
"No consecutive dashes": "Χωρίς διαδοχικές παύλες",
Next: "Επόμενο",
"The slug is already taken. Please choose another slug.": "Ο slug έχει ήδη πιαστεί. Επιλέξτε άλλο slug.",
"No Proxy": "Οχι Proxy",
Authentication: "Authentication",
"HTTP Basic Auth": "HTTP Basic Auth",
"New Status Page": "Νέας Σελίδα κατάστασης",
"Page Not Found": "Η σελίδα δεν βρέθηκε",
"Reverse Proxy": "Αντίστροφο Proxy",
Backup: "Αντιγράφων ασφαλείας",
About: "Σχετικά με το Uptime Kuma",
wayToGetCloudflaredURL: "(Λήψη cloudflared από {0})",
cloudflareWebsite: "Ιστοσελίδα Cloudflare",
"Message:": "Μήνυμα:",
"Don't know how to get the token? Please read the guide:": "Δεν ξέρετε πώς να αποκτήσετε το token; Διαβάστε τον οδηγό:",
"The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "Η τρέχουσα σύνδεση μπορεί να χαθεί εάν αυτή τη στιγμή συνδέεστε μέσω του Cloudflare Tunnel. Θέλετε σίγουρα να το σταματήσετε; Πληκτρολογήστε τον τρέχοντα κωδικό πρόσβασής σας για να τον επιβεβαιώσετε.",
"HTTP Headers": "HTTP Headers",
"Trust Proxy": "Εμπιστοσύνη του Proxy",
"Other Software": "Other Software",
"For example: nginx, Apache and Traefik.": "Για παράδειγμα: nginx, Apache και Traefik.",
"Please read": "Παρακαλώ διαβάστε",
"Subject:": "Θέμα:",
"Valid To:": "Εγκυρο για:",
"Days Remaining:": "Ημέρες που απομένουν:",
"Issuer:": "Εκδότης:",
"Fingerprint:": "Δακτυλικό αποτύπωμα:",
"No status pages": "Δεν υπάρχουν σελίδες κατάστασης",
"Domain Name Expiry Notification": "Ειδοποίηση λήξης ονόματος τομέα",
Proxy: "Proxy",
"Date Created": "Ημερομηνία Δημιουργίας",
HomeAssistant: "Home Assistant",
onebotHttpAddress: "OneBot HTTP Address",
onebotMessageType: "OneBot Message Type",
onebotGroupMessage: "Group",
onebotPrivateMessage: "Private",
onebotUserOrGroupId: "Group/User ID",
onebotSafetyTips: "Για ασφάλεια, πρέπει να ορίσετε το acess token",
"PushDeer Key": "PushDeer Key",
"Footer Text": "Κείμενο υποσέλιδου",
"Show Powered By": "Εμφάνιση Powered By",
"Domain Names": "Ονόματα Τομέα",
signedInDisp: "Συνδεθήκατε ως {0}",
signedInDispDisabled: "Εξουσιοδότηση είναι απενεργοποιημένη.",
RadiusSecret: "Radius Secret",
RadiusSecretDescription: "Shared Secret μεταξύ client και το server",
RadiusCalledStationId: "Called Station Id",
RadiusCalledStationIdDescription: "Identifier της καλούμενης συσκευής",
RadiusCallingStationId: "Calling Station Id",
RadiusCallingStationIdDescription: "Identifier oτης συσκευής κλήσης",
"Certificate Expiry Notification": "Ειδοποίηση Λήξης Πιστοποιητικού",
"API Username": "API Username",
"API Key": "API Key",
"Recipient Number": "Αριθμός Παραλήπτη",
"From Name/Number": "Από Όνομα/Αριθμός",
"Leave blank to use a shared sender number.": "Αφήστε το κενό για να χρησιμοποιήσετε έναν κοινόχρηστο αριθμό αποστολέα.",
"Octopush API Version": "Octopush API Version",
"Legacy Octopush-DM": "Legacy Octopush-DM",
endpoint: "endpoint",
octopushAPIKey: "\"API key\" από το HTTP API credentials στον πίνακα ελέγχου",
octopushLogin: "\"Login\" από το HTTP API credentials στον πίνακα ελέγχου",
promosmsLogin: "API Login Name",
promosmsPassword: "API Password",
"pushoversounds pushover": "Pushover (default)",
"pushoversounds bike": "Bike",
"pushoversounds bugle": "Bugle",
"pushoversounds cashregister": "Cash Register",
"pushoversounds classical": "Classical",
"pushoversounds cosmic": "Cosmic",
"pushoversounds falling": "Falling",
"pushoversounds gamelan": "Gamelan",
"pushoversounds incoming": "Incoming",
"pushoversounds intermission": "Intermission",
"pushoversounds magic": "Magic",
"pushoversounds mechanical": "Mechanical",
"pushoversounds pianobar": "Piano Bar",
"pushoversounds siren": "Siren",
"pushoversounds spacealarm": "Space Alarm",
"pushoversounds tugboat": "Tug Boat",
"pushoversounds alien": "Alien Alarm (long)",
"pushoversounds climb": "Climb (long)",
"pushoversounds persistent": "Persistent (long)",
"pushoversounds echo": "Pushover Echo (long)",
"pushoversounds updown": "Up Down (long)",
"pushoversounds vibrate": "Vibrate Only",
"pushoversounds none": "None (silent)",
pushyAPIKey: "Μυστικό API Key",
pushyToken: "Τoken Συσκευής",
"Show update if available": "Εμφάνιση ενημέρωσης εάν είναι διαθέσιμη",
"Also check beta release": "Ελέγξτε επίσης την έκδοση beta",
"Using a Reverse Proxy?": "Χρησιμοποιείτε reverse proxy;",
"Check how to config it for WebSocket": "Ελέγξτε πώς να το ρυθμίσετε για το WebSocket",
"Steam Game Server": "Διακομιστής παιχνιδιών Steam",
"Most likely causes:": "Πιο πιθανές αιτίες:",
"The resource is no longer available.": "Ο πόρος δεν είναι πλέον διαθέσιμος.",
"There might be a typing error in the address.": "Μπορεί να υπάρχει σφάλμα πληκτρολόγησης στη διεύθυνση.",
"What you can try:": "Τι μπορείτε να δοκιμάσετε:",
"Retype the address.": "Πληκτρολογήστε ξανά τη διεύθυνση.",
"Go back to the previous page.": "Επιστρέψτε στην προηγούμενη σελίδα.",
"Coming Soon": "Ερχεται σύντομα",
wayToGetClickSendSMSToken: "Μπορείτε να πάρετε το API Username και API Key απο {0} .",
"Connection String": "Connection String",
Query: "Query",
settingsCertificateExpiry: "Λήξη πιστοποιητικού TLS",
certificationExpiryDescription: "Οι παρακολουθήσεις HTTPS ενεργοποιούν ειδοποίηση όταν λήξει το πιστοποιητικό TLS σε:",
"Setup Docker Host": "Ρύθμιση Docker Host",
"Connection Type": "Τύπος σύνδεσης",
"Docker Daemon": "Docker Daemon",
deleteDockerHostMsg: "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτόν τον κεντρικό υπολογιστή βάσης για όλες τις παρακολουθήσεις;",
socket: "Socket",
tcp: "TCP / HTTP",
"Docker Container": "Docker Container",
"Container Name / ID": "Container Name / ID",
"Docker Host": "Docker Host",
"Docker Hosts": "Docker Hosts",
"ntfy Topic": "ntfy Topic",
Domain: "Domain",
Workstation: "Workstation",
disableCloudflaredNoAuthMsg: "Βρίσκεστε σε λειτουργία No Auth, δεν απαιτείται κωδικός πρόσβασης.",
trustProxyDescription: "Εμπιστευτείτε τις κεφαλίδες 'X-Forwarded-*'. Εάν θέλετε να λάβετε τη σωστή IP πελάτη και το Uptime Kuma σας βρίσκεται πίσω το Nginx ή το Apache, θα πρέπει να το ενεργοποιήσετε.",
wayToGetLineNotifyToken: "Μπορείτε να λάβετε ένα access token από το {0}",
Examples: "Παραδείγματα",
"Home Assistant URL": "Home Assistant URL",
"Long-Lived Access Token": "Long-Lived Access Token",
"Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "Long-Lived Access Token μπορεί να δημιουργηθεί κάνοντας κλικ στο όνομα του προφίλ σας (κάτω αριστερά) και κάνοντας κύλιση προς τα κάτω και, στη συνέχεια, κάντε κλικ στο Create Token. ",
"Notification Service": "Υπηρεσία ειδοποιήσεων",
"default: notify all devices": "προεπιλογή: ειδοποίηση όλων των συσκευών",
"A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.": "Μπορείτε να βρείτε μια λίστα με τις Υπηρεσίες ειδοποιήσεων στον Home assistant στην περιοχή \"Developer Tools > Services\" αναζήτηση για \"notification\" για να βρείτε το όνομα της συσκευής/τηλεφώνου σας.",
"Automations can optionally be triggered in Home Assistant:": "Οι αυτοματισμοί μπορούν προαιρετικά να ενεργοποιηθούν στο Home Assistant:",
"Trigger type:": "Τύπος ενεργοποίησης:",
"Event type:": "Τύπος συμβάντος:",
"Event data:": "Δεδομένα συμβάντος:",
"Then choose an action, for example switch the scene to where an RGB light is red.": "Στη συνέχεια, επιλέξτε μια ενέργεια, για παράδειγμα αλλάξτε τη σκηνή στο σημείο όπου ένα φως RGB είναι κόκκινο.",
"Frontend Version": "Έκδοση Frontend",
"Frontend Version do not match backend version!": "Η Frontend έκδοση δεν ταιριάζει με την έκδοση backend!",
"Base URL": "Βασική διεύθυνση URL",
goAlertInfo: "Το GoAlert είναι μια εφαρμογή ανοιχτού κώδικα για προγραμματισμό κλήσεων, αυτοματοποιημένες κλιμακώσεις και ειδοποιήσεις (όπως SMS ή φωνητικές κλήσεις). Αλληλεπιδράστε αυτόματα με το σωστό άτομο, με τον σωστό τρόπο και τη σωστή στιγμή! {0}",
goAlertIntegrationKeyInfo: "Λάβετε το generic API integration key για την υπηρεσία σε αυτήν τη μορφή \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" συνήθως την τιμή της παραμέτρου διακριτικού της αντιγραμμένης διεύθυνσης URL.",
goAlert: "GoAlert",
backupOutdatedWarning: "Καταργήθηκε: Επειδή προστέθηκαν πολλές δυνατότητες και αυτή η δυνατότητα δημιουργίας αντιγράφων ασφαλείας δεν διατηρείται πολη, δεν μπορεί να δημιουργήσει ή να επαναφέρει ένα πλήρες αντίγραφο ασφαλείας.",
backupRecommend: "Παρακαλούμε δημιουργήστε αντίγραφα ασφαλείας του volume ή του φακέλου δεδομένων (./data/) απευθείας.",
};

View file

@ -9,11 +9,24 @@ export default {
upsideDownModeDescription: "Flip the status upside down. If the service is reachable, it is DOWN.", upsideDownModeDescription: "Flip the status upside down. If the service is reachable, it is DOWN.",
maxRedirectDescription: "Maximum number of redirects to follow. Set to 0 to disable redirects.", maxRedirectDescription: "Maximum number of redirects to follow. Set to 0 to disable redirects.",
acceptedStatusCodesDescription: "Select status codes which are considered as a successful response.", acceptedStatusCodesDescription: "Select status codes which are considered as a successful response.",
Maintenance: "Maintenance",
statusMaintenance: "Maintenance",
"Schedule maintenance": "Schedule maintenance",
"Affected Monitors": "Affected Monitors",
"Pick Affected Monitors...": "Pick Affected Monitors...",
"Start of maintenance": "Start of maintenance",
"All Status Pages": "All Status Pages",
"Select status pages...": "Select status pages...",
recurringIntervalMessage: "Run once every day | Run once every {0} days",
affectedMonitorsDescription: "Select monitors that are affected by current maintenance",
affectedStatusPages: "Show this maintenance message on selected status pages",
atLeastOneMonitor: "Select at least one affected monitor",
passwordNotMatchMsg: "The repeat password does not match.", passwordNotMatchMsg: "The repeat password does not match.",
notificationDescription: "Notifications must be assigned to a monitor to function.", notificationDescription: "Notifications must be assigned to a monitor to function.",
keywordDescription: "Search keyword in plain HTML or JSON response. The search is case-sensitive.", keywordDescription: "Search keyword in plain HTML or JSON response. The search is case-sensitive.",
pauseDashboardHome: "Pause", pauseDashboardHome: "Pause",
deleteMonitorMsg: "Are you sure want to delete this monitor?", deleteMonitorMsg: "Are you sure want to delete this monitor?",
deleteMaintenanceMsg: "Are you sure want to delete this maintenance?",
deleteNotificationMsg: "Are you sure want to delete this notification for all monitors?", deleteNotificationMsg: "Are you sure want to delete this notification for all monitors?",
dnsPortDescription: "DNS server port. Defaults to 53. You can change the port at any time.", dnsPortDescription: "DNS server port. Defaults to 53. You can change the port at any time.",
resolverserverDescription: "Cloudflare is the default server. You can change the resolver server anytime.", resolverserverDescription: "Cloudflare is the default server. You can change the resolver server anytime.",
@ -365,6 +378,16 @@ export default {
serwersmsAPIPassword: "API Password", serwersmsAPIPassword: "API Password",
serwersmsPhoneNumber: "Phone number", serwersmsPhoneNumber: "Phone number",
serwersmsSenderName: "SMS Sender Name (registered via customer portal)", serwersmsSenderName: "SMS Sender Name (registered via customer portal)",
smseagle: "SMSEagle",
smseagleTo: "Phone number(s)",
smseagleGroup: "Phonebook group name(s)",
smseagleContact: "Phonebook contact name(s)",
smseagleRecipientType: "Recipient type",
smseagleRecipient: "Recipient(s) (multiple must be separated with comma)",
smseagleToken: "API Access token",
smseagleUrl: "Your SMSEagle device URL",
smseagleEncoding: "Send as Unicode",
smseaglePriority: "Message priority (0-9, default = 0)",
stackfield: "Stackfield", stackfield: "Stackfield",
Customize: "Customize", Customize: "Customize",
"Custom Footer": "Custom Footer", "Custom Footer": "Custom Footer",
@ -582,5 +605,41 @@ export default {
goAlert: "GoAlert", goAlert: "GoAlert",
backupOutdatedWarning: "Deprecated: Since a lot of features added and this backup feature is a bit unmaintained, it cannot generate or restore a complete backup.", backupOutdatedWarning: "Deprecated: Since a lot of features added and this backup feature is a bit unmaintained, it cannot generate or restore a complete backup.",
backupRecommend: "Please backup the volume or the data folder (./data/) directly instead.", backupRecommend: "Please backup the volume or the data folder (./data/) directly instead.",
"Optional": "Optional",
squadcast: "Squadcast",
SendKey: "SendKey",
"SMSManager API Docs": "SMSManager API Docs ",
"Gateway Type": "Gateway Type",
SMSManager: "SMSManager",
"You can divide numbers with": "You can divide numbers with",
"or": "or",
recurringInterval: "Interval",
"Recurring": "Recurring",
strategyManual: "Active/Inactive Manually",
warningTimezone: "It is using the server's timezone",
weekdayShortMon: "Mon",
weekdayShortTue: "Tue",
weekdayShortWed: "Wed",
weekdayShortThu: "Thu",
weekdayShortFri: "Fri",
weekdayShortSat: "Sat",
weekdayShortSun: "Sun",
dayOfWeek: "Day of Week",
dayOfMonth: "Day of Month",
lastDay: "Last Day",
lastDay1: "Last Day of Month",
lastDay2: "2nd Last Day of Month",
lastDay3: "3rd Last Day of Month",
lastDay4: "4th Last Day of Month",
"No Maintenance": "No Maintenance",
pauseMaintenanceMsg: "Are you sure want to pause?",
"maintenanceStatus-under-maintenance": "Under Maintenance",
"maintenanceStatus-inactive": "Inactive",
"maintenanceStatus-scheduled": "Scheduled",
"maintenanceStatus-ended": "Ended",
"maintenanceStatus-unknown": "Unknown",
"Display Timezone": "Display Timezone",
"Server Timezone": "Server Timezone",
statusPageMaintenanceEndDate: "End",
IconUrl: "Icon URL", IconUrl: "Icon URL",
}; };

View file

@ -200,7 +200,7 @@ export default {
chatIDNotFound: "ID du salon introuvable, envoyez un message via le bot avant", chatIDNotFound: "ID du salon introuvable, envoyez un message via le bot avant",
webhook: "Webhook", webhook: "Webhook",
"Post URL": "Post URL", "Post URL": "Post URL",
"Content Type": "Content Type", "Content Type": "Type de contenu",
webhookJsonDesc: "{0} est bien/bon pour tous les serveurs HTTP modernes comme express.js", webhookJsonDesc: "{0} est bien/bon pour tous les serveurs HTTP modernes comme express.js",
webhookFormDataDesc: "{multipart} est bien/bon pour du PHP, vous avez juste besoin de mettre le json via/depuis {decodeFunction}", webhookFormDataDesc: "{multipart} est bien/bon pour du PHP, vous avez juste besoin de mettre le json via/depuis {decodeFunction}",
smtp: "Email (SMTP)", smtp: "Email (SMTP)",
@ -227,8 +227,8 @@ export default {
wayToCheckSignalURL: "Vous pouvez regarder l'URL sur comment le mettre en place :", wayToCheckSignalURL: "Vous pouvez regarder l'URL sur comment le mettre en place :",
signalImportant: "IMPORTANT : Vous ne pouvez pas mixer les groupes et les numéros en destinataires !", signalImportant: "IMPORTANT : Vous ne pouvez pas mixer les groupes et les numéros en destinataires !",
gotify: "Gotify", gotify: "Gotify",
"Application Token": "Application Token", "Application Token": "Jeton d'application",
"Server URL": "Server URL", "Server URL": "URL du serveur",
Priority: "Priorité", Priority: "Priorité",
slack: "Slack", slack: "Slack",
"Icon Emoji": "Icon Emoji", "Icon Emoji": "Icon Emoji",
@ -287,7 +287,7 @@ export default {
promosmsTypeSpeed: "SMS SPEED - La plus haute des priorités dans le système. Très rapide et fiable mais cher (environ le double du prix d'un SMS FULL).", promosmsTypeSpeed: "SMS SPEED - La plus haute des priorités dans le système. Très rapide et fiable mais cher (environ le double du prix d'un SMS FULL).",
promosmsPhoneNumber: "Numéro de téléphone (Poiur les déstinataires Polonais, vous pouvez enlever les codes interna.)", promosmsPhoneNumber: "Numéro de téléphone (Poiur les déstinataires Polonais, vous pouvez enlever les codes interna.)",
promosmsSMSSender: "SMS Expéditeur : Nom pré-enregistré ou l'un de base : InfoSMS, SMS Info, MaxSMS, INFO, SMS", promosmsSMSSender: "SMS Expéditeur : Nom pré-enregistré ou l'un de base : InfoSMS, SMS Info, MaxSMS, INFO, SMS",
"Primary Base URL": "Primary Base URL", "Primary Base URL": "URL principale",
emailCustomSubject: "Sujet personalisé", emailCustomSubject: "Sujet personalisé",
clicksendsms: "ClickSend SMS", clicksendsms: "ClickSend SMS",
checkPrice: "Vérification {0} tarifs :", checkPrice: "Vérification {0} tarifs :",
@ -342,13 +342,13 @@ export default {
Title: "Titre", Title: "Titre",
Content: "Contenu", Content: "Contenu",
Style: "Style", Style: "Style",
info: "info", info: "Info",
warning: "Attention", warning: "Attention",
danger: "danger", danger: "Danger",
error: "Erreur", error: "Erreur",
critical: "critique", critical: "Critique",
primary: "primaire", primary: "Primaire",
light: "blanc", light: "Blanc",
dark: "Noir", dark: "Noir",
Post: "Post", Post: "Post",
"Please input title and content": "Veuillez entrer le titre et le contenu", "Please input title and content": "Veuillez entrer le titre et le contenu",
@ -390,7 +390,7 @@ export default {
Installed: "Installé", Installed: "Installé",
"Not installed": "Pas installé", "Not installed": "Pas installé",
"Remove Token": "Supprimer le jeton", "Remove Token": "Supprimer le jeton",
Slug: "chemin", Slug: "Chemin",
"The slug is already taken. Please choose another slug.": "Le chemin est déjà pris. Veuillez choisir un autre chemin.", "The slug is already taken. Please choose another slug.": "Le chemin est déjà pris. Veuillez choisir un autre chemin.",
Authentication: "Authentication", Authentication: "Authentication",
"Page Not Found": "Page non trouvée", "Page Not Found": "Page non trouvée",
@ -431,4 +431,157 @@ export default {
"Trigger type:": "Type de déclencheur:", "Trigger type:": "Type de déclencheur:",
"Event type:": "Type d'événement:", "Event type:": "Type d'événement:",
"Event data:": "Données d'événement:", "Event data:": "Données d'événement:",
topic: "Topic",
topicExplanation: "MQTT sujet à surveiller",
successMessage: "Message de réussite",
successMessageExplanation: "MQTT message qui sera considéré comme un succès",
"Powered by": "Propulsé par",
serwersms: "SerwerSMS.pl",
stackfield: "Stackfield",
smtpDkimSettings: "Paramètres DKIM",
smtpDkimDesc: "Veuillez vous référer au Nodemailer DKIM {0} pour l'utilisation.",
documentation: "Documentation",
smtpDkimDomain: "Nom de domaine",
smtpDkimKeySelector: "Sélecteur de clé",
smtpDkimPrivateKey: "Clé privée",
smtpDkimHashAlgo: "Algorithme de hachage (facultatif)",
smtpDkimheaderFieldNames: "Clés d'en-tête à signer (facultatif)",
smtpDkimskipFields: "Clés d'en-tête à ne pas signer (facultatif)",
wayToGetPagerDutyKey: "Vous pouvez l'obtenir en allant dans Service -> Annuaire des services -> (Sélectionner un service) -> Intégrations -> Ajouter une intégration. Ici, vous pouvez rechercher \"Events API V2\". Plus d'infos {0}",
"Integration Key": "Clé d'intégration",
"Integration URL": "URL d'intégration",
"Auto resolve or acknowledged": "Résolution automatique ou accusé de réception",
"do nothing": "ne fais rien",
"auto acknowledged": "accusé de réception automatique",
"auto resolve": "résolution automatique",
AccessKeyId: "ID de clé d'accès",
SecretAccessKey: "Clé secrète d'accès",
PhoneNumbers: "Les numéros de téléphone",
SignName: "Signature",
"Sms template must contain parameters: ": "Le modèle de SMS doit contenir des paramètres : ",
SecretKey: "Clé secrète",
"For safety, must use secret key": "Pour la sécurité, doit utiliser la clé secrète",
"Device Token": "Jeton d'appareil",
Platform: "Plateforme",
Retry: "Recommencez",
Topic: "Topic",
"Proxy server has authentication": "Le serveur proxy a une authentification",
Running: "Fonctionne",
"Not running": "Ne fonctionne pas",
Start: "Start",
Stop: "Stop",
"Uptime Kuma": "Uptime Kuma",
"No Proxy": "Pas de Proxy",
"HTTP Basic Auth": "Authentification de base HTTP",
"Reverse Proxy": "Proxy inverse",
wayToGetCloudflaredURL: "(Télécharger cloudflared depuis {0})",
cloudflareWebsite: "le site Cloudflare ",
"Message:": "Message:",
"Don't know how to get the token? Please read the guide:": "Vous ne savez pas comment obtenir le jeton ? Veuillez lire le guide:",
"The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "La connexion actuelle peut être perdue si vous vous connectez actuellement via Cloudflare Tunnel. Êtes-vous sûr de vouloir l'arrêter ? Tapez votre mot de passe actuel pour le confirmer.",
"HTTP Headers": "En-têtes HTTP",
"Trust Proxy": "Proxy de confiance",
"Other Software": "Autres logiciels",
"For example: nginx, Apache and Traefik.": "Par exemple : nginx, Apache et Traefik.",
"Please read": "S'il vous plaît Lisez",
"Valid To:": "Valable pour:",
"Days Remaining:": "Jours restant:",
"Domain Name Expiry Notification": "Notification d'expiration de nom de domaine",
"Date Created": "Date de création",
HomeAssistant: "Home Assistant",
onebotHttpAddress: "Adresse HTTP OneBot",
onebotMessageType: "Type de message OneBot",
onebotGroupMessage: "Groupe",
onebotUserOrGroupId: "ID de groupe/utilisateur",
onebotSafetyTips: "Pour des raisons de sécurité, vous devez définir un jeton d'accès",
"PushDeer Key": "Clé PushDeer",
"Show Powered By": "Afficher \"Propulsé par\"",
RadiusSecretDescription: "Secret partagé entre le client et le serveur",
RadiusCalledStationId: "Identifiant de la station appelée",
RadiusCalledStationIdDescription: "Identifiant de l'appareil appelé",
RadiusCallingStationId: "Identifiant de la station appelante",
RadiusCallingStationIdDescription: "Identifiant de l'appareil appelant",
"Certificate Expiry Notification": "Notification d'expiration du certificat",
"API Username": "Nom d'utilisateur de l'API",
"API Key": "clé API",
"Recipient Number": "Numéro du destinataire",
"From Name/Number": "De Nom/Numéro",
"Leave blank to use a shared sender number.": "Laisser vide pour utiliser un numéro d'expéditeur partagé.",
"Octopush API Version": "Version de l'API Octopush",
octopushAPIKey: "\"Clé API\" à partir des informations d'identification de l'API HTTP dans le panneau de configuration",
octopushLogin: "\"Connexion\" à partir des informations d'identification de l'API HTTP dans le panneau de configuration",
"Using a Reverse Proxy?": "Utiliser un proxy inverse ?",
"Check how to config it for WebSocket": "Vérifiez comment le configurer pour WebSocket",
wayToGetClickSendSMSToken: "Vous pouvez obtenir le nom d'utilisateur API et la clé API à partir de {0} .",
"Connection String": "Chaîne de connexion",
Query: "Requête",
tcp: "TCP / HTTP",
"Docker Container": "Conteneur Docker",
Workstation: "Poste de travail",
disableCloudflaredNoAuthMsg: "Vous êtes en mode No Auth, un mot de passe n'est pas nécessaire.",
"Long-Lived Access Token": "Jeton d'accès de longue durée",
"Then choose an action, for example switch the scene to where an RGB light is red.": "Ensuite, choisissez une action, par exemple basculer la scène là où une lumière RVB est rouge.",
"Frontend Version": "Frontend Version",
"Frontend Version do not match backend version!": "La version frontale ne correspond pas à la version principale !",
"Base URL": "URL de base",
goAlertInfo: "GoAlert est une application open source pour la planification des appels, les escalades automatisées et les notifications (comme les SMS ou les appels vocaux). Engagez automatiquement la bonne personne, de la bonne manière et au bon moment ! {0}",
goAlertIntegrationKeyInfo: "Obtenez la clé d'intégration d'API générique pour le service dans ce format \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" généralement la valeur du paramètre de jeton de l'URL copiée.",
goAlert: "GoAlert",
backupOutdatedWarning: "Obsolète : étant donné que de nombreuses fonctionnalités ont été ajoutées et que cette fonctionnalité de sauvegarde est un peu non maintenue, elle ne peut pas générer ou restaurer une sauvegarde complète.",
backupRecommend: "Veuillez sauvegarder le volume ou le dossier de données (./data/) directement à la place.",
Optional: "Optionnel",
squadcast: "Squadcast",
Maintenance: "Maintenance",
statusMaintenance: "Maintenance",
"Schedule maintenance": "Planifier la maintenance",
"Affected Monitors": "Moniteurs concernés",
"Pick Affected Monitors...": "Sélectionnez les moniteurs concernés...",
"Start of maintenance": "Début de la maintenance",
"All Status Pages": "Toutes les pages d'état",
"Select status pages...": "Sélectionnez les pages d'état...",
recurringIntervalMessage: "Exécuter une fois par jour | Exécuter une fois tous les {0} jours",
affectedMonitorsDescription: "Sélectionnez les moniteurs concernés par la maintenance en cours",
affectedStatusPages: "Afficher ce message de maintenance sur les pages d'état sélectionnées",
atLeastOneMonitor: "Sélectionnez au moins un moniteur concerné",
deleteMaintenanceMsg: "Voulez-vous vraiment supprimer cette maintenance ?",
pushyAPIKey: "Clé API secrète",
pushyToken: "Jeton d'appareil",
"You can divide numbers with": "Vous pouvez diviser des nombres avec",
or: "ou",
recurringInterval: "Intervalle",
Recurring: "Récurrent",
"Single Maintenance Window": "Fenêtre de maintenance unique",
"Maintenance Time Window of a Day": "Fenêtre de temps de maintenance",
"Effective Date Range": "Plage de dates d'effet",
strategyManual: "activer/desactiver manuellement",
warningTimezone: "Il utilise le fuseau horaire du serveur",
weekdayShortMon: "Lun",
weekdayShortTue: "Mar",
weekdayShortWed: "Mer",
weekdayShortThu: "Jeu",
weekdayShortFri: "Ven",
weekdayShortSat: "Sam",
weekdayShortSun: "Dim",
dayOfWeek: "Jour de la semaine",
dayOfMonth: "Jour du mois",
lastDay: "Dernier jour",
lastDay1: "Dernier jour du mois",
lastDay2: "2ème dernier jour du mois",
lastDay3: "3ème dernier jour du mois",
lastDay4: "4ème dernier jour du mois",
"No Maintenance": "Aucune Maintenance",
pauseMaintenanceMsg: "Voulez-vous vraiment mettre en pause ?",
"maintenanceStatus-under-maintenance": "En maintenance",
"maintenanceStatus-inactive": "Inactif",
"maintenanceStatus-scheduled": "Programmé",
"maintenanceStatus-ended": "Terminé",
"maintenanceStatus-unknown": "Inconnue",
"Display Timezone": "Afficher le fuseau horaire",
"Server Timezone": "Fuseau horaire du serveur",
"Date and Time": "Date et heure",
"DateTime Range": "Plage de dates et d'heures",
Strategy: "Stratégie",
statusPageMaintenanceEndDate: "Fin",
"Free Mobile User Identifier": "Identifiant d'utilisateur Free Mobile",
"Free Mobile API Key": "Clé API Free Mobile",
}; };

View file

@ -27,8 +27,8 @@ export default {
confirmImportMsg: "Apakah Anda yakin untuk mengimpor cadangan? Pastikan Anda telah memilih opsi impor yang tepat.", confirmImportMsg: "Apakah Anda yakin untuk mengimpor cadangan? Pastikan Anda telah memilih opsi impor yang tepat.",
twoFAVerifyLabel: "Silakan ketik token Anda untuk memverifikasi bahwa 2FA berfungsi", twoFAVerifyLabel: "Silakan ketik token Anda untuk memverifikasi bahwa 2FA berfungsi",
tokenValidSettingsMsg: "Token benar! Anda sekarang dapat menyimpan pengaturan 2FA.", tokenValidSettingsMsg: "Token benar! Anda sekarang dapat menyimpan pengaturan 2FA.",
confirmEnableTwoFAMsg: "Apakah anda yakin ingin mengaktifkan 2FA?", confirmEnableTwoFAMsg: "Apakah Anda yakin ingin mengaktifkan 2FA?",
confirmDisableTwoFAMsg: "Apakah anda yakin ingin menonaktifkan 2FA?", confirmDisableTwoFAMsg: "Apakah Anda yakin ingin menonaktifkan 2FA?",
Settings: "Pengaturan", Settings: "Pengaturan",
Dashboard: "Dasbor", Dashboard: "Dasbor",
"New Update": "Pembaruan Baru", "New Update": "Pembaruan Baru",
@ -126,11 +126,11 @@ export default {
"Resolver Server": "Resolver Server", "Resolver Server": "Resolver Server",
"Resource Record Type": "Resource Record Type", "Resource Record Type": "Resource Record Type",
"Last Result": "Hasil Terakhir", "Last Result": "Hasil Terakhir",
"Create your admin account": "Buat admin akun Anda", "Create your admin account": "Buat akun admin Anda",
"Repeat Password": "Ulangi Sandi", "Repeat Password": "Ulangi Sandi",
"Import Backup": "Impor Cadangan", "Import Backup": "Impor Cadangan",
"Export Backup": "Expor Cadangan", "Export Backup": "Ekspor Cadangan",
Export: "Expor", Export: "Ekspor",
Import: "Impor", Import: "Impor",
respTime: "Tanggapan. Waktu (milidetik)", respTime: "Tanggapan. Waktu (milidetik)",
notAvailableShort: "N/A", notAvailableShort: "N/A",
@ -192,7 +192,7 @@ export default {
"Status Pages": "Halaman Status", "Status Pages": "Halaman Status",
defaultNotificationName: "{notification} saya Peringatan ({number})", defaultNotificationName: "{notification} saya Peringatan ({number})",
here: "di sini", here: "di sini",
Required: "Dibutuhkan", Required: "Wajib",
telegram: "Telegram", telegram: "Telegram",
"Bot Token": "Bot Token", "Bot Token": "Bot Token",
wayToGetTelegramToken: "Anda dapat mendapatkan token dari {0}.", wayToGetTelegramToken: "Anda dapat mendapatkan token dari {0}.",
@ -210,14 +210,14 @@ export default {
secureOptionNone: "None / STARTTLS (25, 587)", secureOptionNone: "None / STARTTLS (25, 587)",
secureOptionTLS: "TLS (465)", secureOptionTLS: "TLS (465)",
"Ignore TLS Error": "Abaikan Kesalahan TLS", "Ignore TLS Error": "Abaikan Kesalahan TLS",
"From Email": "Dari Surel", "From Email": "Dari Email",
emailCustomSubject: "Subjek", emailCustomSubject: "Subjek",
"To Email": "Ke Surel", "To Email": "Ke Email",
smtpCC: "CC", smtpCC: "CC",
smtpBCC: "BCC", smtpBCC: "BCC",
discord: "Discord", discord: "Discord",
"Discord Webhook URL": "Discord Webhook URL", "Discord Webhook URL": "Discord Webhook URL",
wayToGetDiscordURL: "Anda bisa mendapatkan ini dengan pergi ke Server Settings -> Integrations -> Create Webhook", wayToGetDiscordURL: "Anda bisa mendapatkan ini dengan pergi ke Server Pengaturan -> Integrasi -> Buat Webhook",
"Bot Display Name": "Nama Bot", "Bot Display Name": "Nama Bot",
"Prefix Custom Message": "Awalan Pesan", "Prefix Custom Message": "Awalan Pesan",
"Hello @everyone is...": "Halo {'@'}everyone is...", "Hello @everyone is...": "Halo {'@'}everyone is...",
@ -232,7 +232,7 @@ export default {
signalImportant: "PENTING: Anda tidak dapat mencampur grup dan nomor di penerima!", signalImportant: "PENTING: Anda tidak dapat mencampur grup dan nomor di penerima!",
gotify: "Gotify", gotify: "Gotify",
"Application Token": "Token Aplikasi", "Application Token": "Token Aplikasi",
"Server URL": "URL Peladen", "Server URL": "URL Server",
Priority: "Prioritas", Priority: "Prioritas",
slack: "Slack", slack: "Slack",
"Icon Emoji": "Ikon Emoji", "Icon Emoji": "Ikon Emoji",
@ -274,9 +274,9 @@ export default {
"LunaSea Device ID": "LunaSea Device ID", "LunaSea Device ID": "LunaSea Device ID",
"Apprise URL": "Apprise URL", "Apprise URL": "Apprise URL",
"Example:": "Contoh: {0}", "Example:": "Contoh: {0}",
"Read more:": "Baca lebih lajut: {0}", "Read more:": "Baca lebih lanjut: {0}",
"Status:": "Status: {0}", "Status:": "Status: {0}",
"Read more": "Baca lebih lajut", "Read more": "Baca lebih lanjut",
appriseInstalled: "Apprise diinstall.", appriseInstalled: "Apprise diinstall.",
appriseNotInstalled: "Apprise tidak diinstall. {0}", appriseNotInstalled: "Apprise tidak diinstall. {0}",
"Access Token": "Token Akses", "Access Token": "Token Akses",
@ -310,7 +310,7 @@ export default {
BodyInvalidFormat: "Request Body memiliki format JSON yang tidak sesuai: ", BodyInvalidFormat: "Request Body memiliki format JSON yang tidak sesuai: ",
"Monitor History": "Riyawat Monitor", "Monitor History": "Riyawat Monitor",
clearDataOlderThan: "Simpan data riwayat monitoring selama {0} hari.", clearDataOlderThan: "Simpan data riwayat monitoring selama {0} hari.",
PasswordsDoNotMatch: "Passwords tidak sama.", PasswordsDoNotMatch: "Password tidak sama.",
records: "catatan", records: "catatan",
"One record": "Satu catatan", "One record": "Satu catatan",
steamApiKeyDescription: "Untuk monitoring Steam Game Server Anda membutuhkan kunci Steam Web-API. Anda dapat mendaftarkan Kunci API Anda melalui: ", steamApiKeyDescription: "Untuk monitoring Steam Game Server Anda membutuhkan kunci Steam Web-API. Anda dapat mendaftarkan Kunci API Anda melalui: ",
@ -322,13 +322,13 @@ export default {
recent: "Baru saja", recent: "Baru saja",
Done: "Selesai", Done: "Selesai",
Info: "Info", Info: "Info",
Security: "Keamaan", Security: "Keamanan",
"Steam API Key": "Steam API Key", "Steam API Key": "Steam API Key",
"Shrink Database": "Shrink Database", "Shrink Database": "Shrink Database",
"Pick a RR-Type...": "Pilih RR-Type...", "Pick a RR-Type...": "Pilih RR-Type...",
"Pick Accepted Status Codes...": "Pilih Kode Status yang Diterima...", "Pick Accepted Status Codes...": "Pilih Kode Status yang Diterima...",
Default: "Default", Default: "Default",
"HTTP Options": "HTTP Options", "HTTP Options": "Opsi HTTP",
"Create Incident": "Buat Incident", "Create Incident": "Buat Incident",
Title: "Judul", Title: "Judul",
Content: "Konten", Content: "Konten",
@ -379,8 +379,8 @@ export default {
smtpDkimheaderFieldNames: "Header Keys untuk ditambahkan (Optional)", smtpDkimheaderFieldNames: "Header Keys untuk ditambahkan (Optional)",
smtpDkimskipFields: "Header Keys not untuk ditambahkan (Optional)", smtpDkimskipFields: "Header Keys not untuk ditambahkan (Optional)",
wayToGetPagerDutyKey: "Anda dapat menambahkan melalui Service -> Service Directory -> (Select a service) -> Integrations -> Add integration. Lalu Anda dapat menjadi dengan kata kunci \"Events API V2\". Informasi tambahan {0}", wayToGetPagerDutyKey: "Anda dapat menambahkan melalui Service -> Service Directory -> (Select a service) -> Integrations -> Add integration. Lalu Anda dapat menjadi dengan kata kunci \"Events API V2\". Informasi tambahan {0}",
"Integration Key": "Integration Key", "Integration Key": "Kunci Integrasi",
"Integration URL": "Integration URL", "Integration URL": "URL Integrasi",
"Auto resolve or acknowledged": "Penyelesaian otomatis atau diakui", "Auto resolve or acknowledged": "Penyelesaian otomatis atau diakui",
"do nothing": "tidak melakukan apapun", "do nothing": "tidak melakukan apapun",
"auto acknowledged": "otomatis diakui", "auto acknowledged": "otomatis diakui",
@ -393,7 +393,7 @@ export default {
alertaAlertState: "Status Siaga", alertaAlertState: "Status Siaga",
alertaRecoverState: "Status Pemulihan", alertaRecoverState: "Status Pemulihan",
deleteStatusPageMsg: "Apakah Anda yakin untuk menghapus halaman status berikut?", deleteStatusPageMsg: "Apakah Anda yakin untuk menghapus halaman status berikut?",
Proxies: "Proxies", Proxies: "Proxy",
default: "Bawaan", default: "Bawaan",
enabled: "Diaktifkan", enabled: "Diaktifkan",
setAsDefault: "Tetapkan sebagai bawaan", setAsDefault: "Tetapkan sebagai bawaan",
@ -402,14 +402,14 @@ export default {
enableProxyDescription: "Proxy berikut tidak akan berdampak ke monitor hingga diaktifkan. Anda dapat mengontrol menonaktifkan sementara proxy dari semua monitor dengan status aktivasi.", enableProxyDescription: "Proxy berikut tidak akan berdampak ke monitor hingga diaktifkan. Anda dapat mengontrol menonaktifkan sementara proxy dari semua monitor dengan status aktivasi.",
setAsDefaultProxyDescription: "Proxy berikut akan diaktifkan sebagai bawaan untuk monitor baru. Anda masih dapat menonaktifkan proxy secara terpisah untuk setiap monitor.", setAsDefaultProxyDescription: "Proxy berikut akan diaktifkan sebagai bawaan untuk monitor baru. Anda masih dapat menonaktifkan proxy secara terpisah untuk setiap monitor.",
"Certificate Chain": "Certificate Chain", "Certificate Chain": "Certificate Chain",
Valid: "Sahih", Valid: "Valid",
Invalid: "Tidak Sahih", Invalid: "Tidak Valid",
AccessKeyId: "AccessKey ID", AccessKeyId: "AccessKey ID",
SecretAccessKey: "AccessKey Secret", SecretAccessKey: "AccessKey Secret",
PhoneNumbers: "Nomor Telepon", PhoneNumbers: "Nomor Telepon",
TemplateCode: "Kode Template", TemplateCode: "Kode Template",
SignName: "Nama Tanda", SignName: "Nama Tanda",
"Sms template must contain parameters: ": "Template SMS harus memuat parameter: ", "Sms template must contain parameters: ": "Template SMS harus berisi parameter: ",
"Bark Endpoint": "Bark Endpoint", "Bark Endpoint": "Bark Endpoint",
"Bark Group": "Bark Group", "Bark Group": "Bark Group",
"Bark Sound": "Bark Sound", "Bark Sound": "Bark Sound",
@ -432,8 +432,8 @@ export default {
User: "Pengguna", User: "Pengguna",
Installed: "Terpasang", Installed: "Terpasang",
"Not installed": "Tidak terpasang", "Not installed": "Tidak terpasang",
Running: "Berlari", Running: "Berjalan",
"Not running": "Tidak berlari", "Not running": "Tidak berjalan",
"Remove Token": "Hapus Token", "Remove Token": "Hapus Token",
Start: "Mulai", Start: "Mulai",
Stop: "Berhenti", Stop: "Berhenti",
@ -444,8 +444,8 @@ export default {
startOrEndWithOnly: "Mulai atau akhiri hanya dengan {0}", startOrEndWithOnly: "Mulai atau akhiri hanya dengan {0}",
"No consecutive dashes": "Tanda hubung tidak berurutan", "No consecutive dashes": "Tanda hubung tidak berurutan",
Next: "Selanjutnya", Next: "Selanjutnya",
"The slug is already taken. Please choose another slug.": "Slug is telah digunakan. Silakan pilih slug lain.", "The slug is already taken. Please choose another slug.": "Slug telah digunakan. Silakan pilih slug lain.",
"No Proxy": "TIdak ada Proxy", "No Proxy": "Tidak ada Proxy",
Authentication: "Autentikasi", Authentication: "Autentikasi",
"HTTP Basic Auth": "HTTP Basic Auth", "HTTP Basic Auth": "HTTP Basic Auth",
"New Status Page": "Halaman Status Baru", "New Status Page": "Halaman Status Baru",
@ -457,7 +457,7 @@ export default {
cloudflareWebsite: "Situs Cloudflare", cloudflareWebsite: "Situs Cloudflare",
"Message:": "Pesan:", "Message:": "Pesan:",
"Don't know how to get the token? Please read the guide:": "Tidak tahu cara mendapatkan token? Silakan baca panduannya:", "Don't know how to get the token? Please read the guide:": "Tidak tahu cara mendapatkan token? Silakan baca panduannya:",
"The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "Koneksi saat ini mungkin hilang jika Anda saat ini terhubung melalui CloudflareTunnel. Apakah Anda yakin ingin menghentikannya? Ketik kata sandi Anda saat ini untuk mengonfirmasinya.", "The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "Koneksi saat ini mungkin hilang jika Anda saat ini terhubung melalui Cloudflare Tunel. Apakah Anda yakin ingin menghentikannya? Ketik kata sandi Anda saat ini untuk mengonfirmasinya.",
"HTTP Headers": "HTTP Headers", "HTTP Headers": "HTTP Headers",
"Trust Proxy": "Proxy Terpercaya", "Trust Proxy": "Proxy Terpercaya",
"Other Software": "Perangkat Lunak lainnya", "Other Software": "Perangkat Lunak lainnya",
@ -494,7 +494,7 @@ export default {
"Certificate Expiry Notification": "Pemberitahuan Kedaluwarsa Sertifikat", "Certificate Expiry Notification": "Pemberitahuan Kedaluwarsa Sertifikat",
"API Username": "Nama Pengguna API", "API Username": "Nama Pengguna API",
"API Key": "Kunci API", "API Key": "Kunci API",
"Recipient Number": "Nomor Penerima Recipient Number", "Recipient Number": "Nomor Penerima",
"From Name/Number": "Dari Nama/Nomor", "From Name/Number": "Dari Nama/Nomor",
"Leave blank to use a shared sender number.": "Biarkan kosong untuk menggunakan nomor pengirim bersama.", "Leave blank to use a shared sender number.": "Biarkan kosong untuk menggunakan nomor pengirim bersama.",
"Octopush API Version": "Versi API Octopush", "Octopush API Version": "Versi API Octopush",
@ -542,9 +542,9 @@ export default {
"Go back to the previous page.": "Kembali ke halaman sebelumnya.", "Go back to the previous page.": "Kembali ke halaman sebelumnya.",
"Coming Soon": "Segera", "Coming Soon": "Segera",
wayToGetClickSendSMSToken: "Anda bisa mendapatkan Nama Pengguna API dan Kunci API dari {0} .", wayToGetClickSendSMSToken: "Anda bisa mendapatkan Nama Pengguna API dan Kunci API dari {0} .",
"Connection String": "Connection String", "Connection String": "String Koneksi",
Query: "Query", Query: "Query",
settingsCertificateExpiry: "Kedaluwarsa Sertifikat TLS", settingsCertificateExpiry: "Sertifikat TLS Kadaluarsa",
certificationExpiryDescription: "Monitor HTTPS memicu pemberitahuan saat sertifikat TLS kedaluwarsa dalam:", certificationExpiryDescription: "Monitor HTTPS memicu pemberitahuan saat sertifikat TLS kedaluwarsa dalam:",
"Setup Docker Host": "Siapkan Host Docker", "Setup Docker Host": "Siapkan Host Docker",
"Connection Type": "Jenis Koneksi", "Connection Type": "Jenis Koneksi",
@ -565,14 +565,14 @@ export default {
Examples: "Contoh", Examples: "Contoh",
"Home Assistant URL": "Home Assistant URL", "Home Assistant URL": "Home Assistant URL",
"Long-Lived Access Token": "Token Akses Berumur Panjang", "Long-Lived Access Token": "Token Akses Berumur Panjang",
"Long-Lived Access Token canbe created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "Token Akses Berumur Panjang dapat dibuat dengan mengklik nama profil Anda (kiri bawah) dan menggulir ke bawah lalu klik Buat Token. ", "Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "Token Akses Berumur Panjang dapat dibuat dengan mengklik nama profil Anda (kiri bawah) dan menggulir ke bawah lalu klik Buat Token. ",
"Notification Service": "Layanan Pemberitahuan", "Notification Service": "Layanan Pemberitahuan",
"default: notify all devices": "bawaan: notifikasi seluruh perangkat", "default: notify all devices": "bawaan: notifikasi seluruh perangkat",
"A listof Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.": "Daftar Layanan Pemberitahuan dapat ditemukan di Home Assistant pada \"Developer Tools > Services\" cari \"notification\" lalu cari nama perangkat Anda.", "A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.": "Daftar Layanan Pemberitahuan dapat ditemukan di Home Assistant pada \"Developer Tools > Services\" cari \"notification\" lalu cari nama perangkat Anda.",
"Automations can optionally be triggered in Home Assistant:": "Otomatisasi dapat dipicu secara opsional di Home Assistant:", "Automations can optionally be triggered in Home Assistant:": "Otomatisasi dapat dipicu secara opsional di Home Assistant:",
"Trigger type:": "Trigger type:", "Trigger type:": "Tipe Trigger/Pemicu:",
"Event type:": "Event type:", "Event type:": "Tipe event:",
"Event data:": "Event data:", "Event data:": "Data event:",
"Then choose an action, for example switch the scene to where an RGB light is red.": "Kemudian pilih tindakan, misalnya alihkan ke tempat dimana lampu RGB berwarna merah.", "Then choose an action, for example switch the scene to where an RGB light is red.": "Kemudian pilih tindakan, misalnya alihkan ke tempat dimana lampu RGB berwarna merah.",
"Frontend Version": "Versi Frontend", "Frontend Version": "Versi Frontend",
"Frontend Version do not match backend version!": "Versi Frontend tidak sama dengan versi backend!", "Frontend Version do not match backend version!": "Versi Frontend tidak sama dengan versi backend!",
@ -580,6 +580,6 @@ export default {
goAlertInfo: "GoAlert adalah aplikasi open source untuk penjadwalan panggilan, eskalasi otomatis dan pemberitahuan (seperti SMS atau panggilan suara). Secara otomatis melibatkan orang yang tepat, dengan cara yang benar, dan pada waktu yang tepat! {0}", goAlertInfo: "GoAlert adalah aplikasi open source untuk penjadwalan panggilan, eskalasi otomatis dan pemberitahuan (seperti SMS atau panggilan suara). Secara otomatis melibatkan orang yang tepat, dengan cara yang benar, dan pada waktu yang tepat! {0}",
goAlertIntegrationKeyInfo: "Dapatkan kunci integrasi API generik untuk layanan dalam format ini \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" biasanya nilai parameter token dari URL yang disalin.", goAlertIntegrationKeyInfo: "Dapatkan kunci integrasi API generik untuk layanan dalam format ini \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" biasanya nilai parameter token dari URL yang disalin.",
goAlert: "GoAlert", goAlert: "GoAlert",
backupOutdatedWarning: "Usang: Karena banyak fitur ditambahkan dan fitur cadangan ini agak tidak terawat, itu tidak dapat menghasilkan atau memulihkan cadangan lengkap.", backupOutdatedWarning: "Tidak digunakan lagi: Karena banyak fitur ditambahkan dan fitur cadangan ini agak tidak terawat, itu tidak dapat menghasilkan atau memulihkan cadangan lengkap.",
backupRecommend: "Harap cadangkan volume atau folder data (./data/) secara langsung.", backupRecommend: "Harap cadangkan volume atau folder data (./data/) secara langsung.",
}; };

View file

@ -125,7 +125,7 @@ export default {
Export: "Eksportuj", Export: "Eksportuj",
Import: "Importuj", Import: "Importuj",
respTime: "Czas odp. (ms)", respTime: "Czas odp. (ms)",
notAvailableShort: "N/A", notAvailableShort: "N/D",
"Default enabled": "Włącz domyślnie", "Default enabled": "Włącz domyślnie",
"Apply on all existing monitors": "Zastosuj do istniejących monitorów", "Apply on all existing monitors": "Zastosuj do istniejących monitorów",
Create: "Stwórz", Create: "Stwórz",
@ -181,7 +181,7 @@ export default {
"Edit Status Page": "Edytuj stronę statusu", "Edit Status Page": "Edytuj stronę statusu",
"Go to Dashboard": "Idź do panelu", "Go to Dashboard": "Idź do panelu",
"Status Page": "Strona statusu", "Status Page": "Strona statusu",
"Status Pages": "Strona statusu", "Status Pages": "Strony statusów",
defaultNotificationName: "Moje powiadomienie {notification} ({number})", defaultNotificationName: "Moje powiadomienie {notification} ({number})",
here: "tutaj", here: "tutaj",
Required: "Wymagane", Required: "Wymagane",
@ -359,6 +359,16 @@ export default {
serwersmsAPIPassword: "Hasło API", serwersmsAPIPassword: "Hasło API",
serwersmsPhoneNumber: "Numer telefonu", serwersmsPhoneNumber: "Numer telefonu",
serwersmsSenderName: "Nazwa nadawcy (zatwierdzona w panelu klienta)", serwersmsSenderName: "Nazwa nadawcy (zatwierdzona w panelu klienta)",
smseagle: "SMSEagle",
smseagleTo: "Numer/y telefonu",
smseagleGroup: "Grupa/y z Książki adresowej",
smseagleContact: "Kontakt/y z Książki adresowej",
smseagleRecipientType: "Typ odbiorcy",
smseagleRecipient: "Odbiorca/y (wiele musi być oddzielone przecinkami)",
smseagleToken: "Klucz dostępu API",
smseagleUrl: "URL Twojego urządzenia SMSEagle",
smseagleEncoding: "Wyślij jako Unicode",
smseaglePriority: "Priorytet wiadomości (0-9, domyślnie = 0)",
stackfield: "Stackfield", stackfield: "Stackfield",
Customize: "Dostosuj", Customize: "Dostosuj",
"Custom Footer": "Niestandardowa stopka", "Custom Footer": "Niestandardowa stopka",
@ -435,7 +445,7 @@ export default {
"HTTP Basic Auth": "Podstawowa autoryzacja HTTP", "HTTP Basic Auth": "Podstawowa autoryzacja HTTP",
"New Status Page": "Nowa strona statusu", "New Status Page": "Nowa strona statusu",
"Page Not Found": "Strona nie została znaleziona", "Page Not Found": "Strona nie została znaleziona",
"Reverse Proxy": "Odwrotne Proxy", "Reverse Proxy": "Zwrotny serwer proxy",
Backup: "Backup", Backup: "Backup",
About: "O skrypcie", About: "O skrypcie",
wayToGetCloudflaredURL: "(Pobierz cloudflared z {0})", wayToGetCloudflaredURL: "(Pobierz cloudflared z {0})",
@ -447,7 +457,7 @@ export default {
"For example: nginx, Apache and Traefik.": "Na przykład: nginx, Apache i Traefik.", "For example: nginx, Apache and Traefik.": "Na przykład: nginx, Apache i Traefik.",
"Please read": "Przeczytaj proszę", "Please read": "Przeczytaj proszę",
"Subject:": "Temat:", "Subject:": "Temat:",
"Valid To:": "Ważdny do:", "Valid To:": "Ważny do:",
"Days Remaining:": "Pozostało dni:", "Days Remaining:": "Pozostało dni:",
"Issuer:": "Wydawca:", "Issuer:": "Wydawca:",
"Fingerprint:": "Odcisk palca:", "Fingerprint:": "Odcisk palca:",
@ -467,4 +477,168 @@ export default {
"Domain Names": "Domeny", "Domain Names": "Domeny",
signedInDisp: "Zalogowany jako {0}", signedInDisp: "Zalogowany jako {0}",
signedInDispDisabled: "Autoryzacja wyłączona.", signedInDispDisabled: "Autoryzacja wyłączona.",
resendEveryXTimes: "Wysyłaj ponownie co {0} razy",
resendDisabled: "Ponowne wysyłanie jest wyłączone",
Maintenance: "Konserwacja",
statusMaintenance: "Konserwacja",
"Schedule maintenance": "Planowanie konserwacji",
"Affected Monitors": "Monitory dotknięte problemem",
"Pick Affected Monitors...": "Wybierz monitory, których to dotyczy...",
"Start of maintenance": "Rozpoczęcie konserwacji",
"All Status Pages": "Wszystkie strony statusu",
"Select status pages...": "Wybierz strony statusu...",
recurringIntervalMessage: "Uruchom raz dziennie | Uruchom raz na {0} dni",
affectedMonitorsDescription: "Wybierz monitory, których dotyczy bieżąca konserwacja",
affectedStatusPages: "Pokaż ten komunikat o konserwacji na wybranych stronach statusu",
atLeastOneMonitor: "Wybierz co najmniej jeden monitor, którego dotyczy problem",
deleteMaintenanceMsg: "Czy na pewno chcesz usunąć tę konserwację?",
dnsPortDescription: "Port serwera DNS. Domyślnie 53. Możesz zmienić port w dowolnym momencie.",
"Resend Notification if Down X times consequently": "Wyślij ponownie powiadomienie, jeśli nie działa X razy pod rząd",
error: "błąd",
critical: "krytyczny",
wayToGetPagerDutyKey: "Możesz to uzyskać, przechodząc do Service -> Service Directory -> (wybierz usługę) -> Integrations -> Add integration. Tutaj możesz wyszukać \"Events API V2\". Więcej informacji {0}",
"Integration Key": "Klucz integracji",
"Integration URL": "Adres URL integracji",
"Auto resolve or acknowledged": "Automatycznie rozwiązany lub potwierdzony",
"do nothing": "nie rób nic",
"auto acknowledged": "auto potwierdzony",
"auto resolve": "automatycznie rozwiązany",
"Bark Group": "Grupa Bark",
"Bark Sound": "Dźwięk Bark",
"HTTP Headers": "Nagłówki HTTP",
"Trust Proxy": "Ufaj proxy",
HomeAssistant: "Home Assistant",
RadiusSecret: "Sekretny klucz Radius",
RadiusSecretDescription: "Współdzielony sekretny klucz pomiędzy klientem a serwerem",
RadiusCalledStationId: "Id stacji wywoływanej",
RadiusCalledStationIdDescription: "Identyfikator wywoływanego urządzenia",
RadiusCallingStationId: "Id stacji wywoławczej",
RadiusCallingStationIdDescription: "Identyfikator urządzenia wywołującego",
"Certificate Expiry Notification": "Powiadomienie o wygaśnięciu certyfikatu",
"API Username": "Nazwa użytkownika API",
"API Key": "Klucz API",
"Recipient Number": "Numer odbiorcy",
"From Name/Number": "Od nazwa/numer",
"Leave blank to use a shared sender number.": "Pozostaw puste, aby użyć wspólnego numeru nadawcy.",
"Octopush API Version": "Wersja API Octopush",
"Legacy Octopush-DM": "Starsze Octopush-DM",
endpoint: "punkt końcowy",
octopushAPIKey: "\"API key\" z poświadczeń HTTP API w panelu sterowania",
octopushLogin: "\"Login\" z poświadczeń HTTP API w panelu sterowania",
promosmsLogin: "Nazwa logowania API",
promosmsPassword: "Hasło API",
"pushoversounds pushover": "Pushover (domyślny)",
"pushoversounds bike": "Bike",
"pushoversounds bugle": "Bugle",
"pushoversounds cashregister": "Cash Register",
"pushoversounds classical": "Classical",
"pushoversounds cosmic": "Cosmic",
"pushoversounds falling": "Falling",
"pushoversounds gamelan": "Gamelan",
"pushoversounds incoming": "Incoming",
"pushoversounds intermission": "Intermission",
"pushoversounds magic": "Magic",
"pushoversounds mechanical": "Mechanical",
"pushoversounds pianobar": "Piano Bar",
"pushoversounds siren": "Siren",
"pushoversounds spacealarm": "Space Alarm",
"pushoversounds tugboat": "Tug Boat",
"pushoversounds alien": "Alien Alarm (długie)",
"pushoversounds climb": "Climb (długie)",
"pushoversounds persistent": "Persistent (długie)",
"pushoversounds echo": "Pushover Echo (długie)",
"pushoversounds updown": "Up Down (długie)",
"pushoversounds vibrate": "Tylko wibracje",
"pushoversounds none": "Brak (cisza)",
pushyAPIKey: "Tajny klucz API",
pushyToken: "Token urządzenia",
"Show update if available": "Pokaż aktualizację, jeśli jest dostępna",
"Also check beta release": "Sprawdź również wydanie beta",
"Using a Reverse Proxy?": "Używasz odwróconego proxy?",
"Check how to config it for WebSocket": "Sprawdź jak go skonfigurować dla WebSocket",
"Steam Game Server": "Serwer gry Steam",
"Most likely causes:": "Najbardziej prawdopodobne przyczyny:",
"The resource is no longer available.": "Zasób nie jest już dostępny.",
"There might be a typing error in the address.": "W adresie może być błąd w pisowni.",
"What you can try:": "Co możesz spróbować:",
"Retype the address.": "Ponownie wpisz adres.",
"Go back to the previous page.": "Wróć do poprzedniej strony.",
"Coming Soon": "Wkrótce",
wayToGetClickSendSMSToken: "Możesz uzyskać nazwę użytkownika API i klucz API z {0}.",
"Connection String": "Ciąg połączenia",
Query: "Zapytanie",
settingsCertificateExpiry: "Wygaśnięcie certyfikatu TLS",
certificationExpiryDescription: "Monitory HTTPS uruchamiają powiadomienia o wygaśnięciu certyfikatu TLS w:",
"Setup Docker Host": "Konfiguracja hosta Docker",
"Connection Type": "Typ połączenia",
"Docker Daemon": "Demon Dockera",
deleteDockerHostMsg: "Czy na pewno chcesz usunąć ten host Dockera dla wszystkich monitorów?",
socket: "Gniazdo",
tcp: "TCP / HTTP",
"Docker Container": "Kontener Dockera",
"Container Name / ID": "Nazwa kontenera / ID",
"Docker Host": "Host Dockera",
"Docker Hosts": "Hosty Dockera",
"ntfy Topic": "Temat ntfy",
Domain: "Domena",
Workstation: "Stacja robocza",
disableCloudflaredNoAuthMsg: "Jesteś w trybie No Auth, hasło nie jest wymagane.",
trustProxyDescription: "Zaufaj nagłówkom 'X-Forwarded-*'. Jeśli chcesz uzyskać poprawne IP klienta, a twój Uptime Kuma jest za Nginx lub Apache, powinieneś to włączyć.",
wayToGetLineNotifyToken: "Możesz uzyskać token dostępu z {0}",
Examples: "Przykłady",
"Home Assistant URL": "URL Home Assistant",
"Long-Lived Access Token": "Długotrwały token dostępu",
"Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "Długotrwały token dostępu można utworzyć klikając na nazwę swojego profilu (na dole po lewej stronie) i przewijając do dołu, a następnie klikając Create Token. ",
"Notification Service": "Usługa powiadamiania",
"default: notify all devices": "domyślnie: powiadamiaj wszystkie urządzenia",
"A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.": "Listę usług powiadamiania można znaleźć w Home Assistant pod \"Developer Tools > Services\" wyszukaj \"notification\", aby znaleźć nazwę swojego urządzenia/telefonu.",
"Automations can optionally be triggered in Home Assistant:": "Automaty mogą być opcjonalnie uruchamiane w Home Assistant:",
"Trigger type:": "Typ wyzwalacza:",
"Event type:": "Typ zdarzenia:",
"Event data:": "Dane o zdarzeniu:",
"Then choose an action, for example switch the scene to where an RGB light is red.": "Następnie wybierz akcję, na przykład przełącz scenę na taką, w której światło RGB jest czerwone.",
"Frontend Version": "Wersja frontu",
"Frontend Version do not match backend version!": "Wersja frontu nie pasuje do wersji backendu!",
"Base URL": "Bazowy adres URL",
goAlertInfo: "GoAlert to aplikacja open source do planowania, automatycznych eskalacji i powiadomień (jak SMS lub połączenia głosowe). Automatycznie angażuj właściwą osobę, we właściwy sposób i we właściwym czasie! {0}",
goAlertIntegrationKeyInfo: "Pobierz generyczny klucz integracyjny API dla usługi, którego wartość skopiowanego tokena URL jest zwykle w formacie \"aaaaaaaa-bbb-cccc-dddd-eeeeee\".",
goAlert: "GoAlert",
backupOutdatedWarning: "Przestarzałe: ponieważ dodano wiele funkcji i funkcja tworzenia kopii zapasowych nie jest wystarczająco utrzymywana, nie może generować ani przywracać pełnej kopii zapasowej.",
backupRecommend: "Zamiast tego należy wykonać bezpośrednią kopię zapasową woluminu lub folderu danych (./data/).",
Optional: "Opcjonalne",
squadcast: "Squadcast",
SendKey: "SendKey",
"SMSManager API Docs": "Dokumentacja API SMSManager ",
"Gateway Type": "Typ bramy",
SMSManager: "SMSManager",
"You can divide numbers with": "Możesz dzielić liczby przez",
or: "lub",
recurringInterval: "odstęp czasu",
Recurring: "powtarzający się",
strategyManual: "Aktywowany/dezaktywowany ręcznie",
warningTimezone: "Używa strefy czasowej serwera",
weekdayShortMon: "pon",
weekdayShortTue: "wt",
weekdayShortWed: "śr",
weekdayShortThu: "czw",
weekdayShortFri: "pt",
weekdayShortSat: "sob",
weekdayShortSun: "niedz",
dayOfWeek: "Dzień tygodnia",
dayOfMonth: "Dzień miesiąca",
lastDay: "Ostatni dzień",
lastDay1: "Ostatni dzień miesiąca",
lastDay2: "2. ostatni dzień miesiąca",
lastDay3: "3. ostatni dzień miesiąca",
lastDay4: "4. ostatni dzień miesiąca",
"No Maintenance": "Brak konserwacji",
pauseMaintenanceMsg: "Jesteś pewien, że chcesz zatrzymać?",
"maintenanceStatus-under-maintenance": "Podczas konserwacji",
"maintenanceStatus-inactive": "Nieaktywny",
"maintenanceStatus-scheduled": "Zaplanowany",
"maintenanceStatus-ended": "Zakończony",
"maintenanceStatus-unknown": "Nieznany",
"Display Timezone": "Wyświetlana strefa czasowa",
"Server Timezone": "Strefa czasowa serwera",
statusPageMaintenanceEndDate: "Koniec",
}; };

View file

@ -393,6 +393,12 @@ export default {
alertaAlertState: "Состояние алерта", alertaAlertState: "Состояние алерта",
alertaRecoverState: "Состояние восстановления", alertaRecoverState: "Состояние восстановления",
Proxies: "Прокси", Proxies: "Прокси",
"Setup Proxy": "Настройка Прокси",
"Proxy Protocol": "Протокол Прокси",
"Proxy Server": "Прокси",
"Proxy server has authentication": "Прокси имеет аутентификацию",
"Reverse Proxy": "Обратный прокси",
"No Proxy": "Без прокси",
default: "По умолчанию", default: "По умолчанию",
enabled: "Включено", enabled: "Включено",
setAsDefault: "Установлено по умолчанию", setAsDefault: "Установлено по умолчанию",
@ -400,4 +406,176 @@ export default {
proxyDescription: "Прокси должны быть привязаны к монитору, чтобы работать.", proxyDescription: "Прокси должны быть привязаны к монитору, чтобы работать.",
enableProxyDescription: "Этот прокси не будет влиять на запросы монитора, пока не будет активирован. Вы можете контролировать временное отключение прокси для всех мониторов через статус активации.", enableProxyDescription: "Этот прокси не будет влиять на запросы монитора, пока не будет активирован. Вы можете контролировать временное отключение прокси для всех мониторов через статус активации.",
setAsDefaultProxyDescription: "Этот прокси будет по умолчанию включен для новых мониторов. Вы всё ещё можете отдельно отключать прокси в каждом мониторе.", setAsDefaultProxyDescription: "Этот прокси будет по умолчанию включен для новых мониторов. Вы всё ещё можете отдельно отключать прокси в каждом мониторе.",
Invalid: "Недействительный",
AccessKeyId: "AccessKey ID",
SecretAccessKey: "AccessKey Secret",
PhoneNumbers: "PhoneNumbers",
TemplateCode: "TemplateCode",
SignName: "SignName",
"Sms template must contain parameters: ": "Шаблон СМС должен содержать параметры: ",
"Bark Endpoint": "Bark Endpoint",
"Bark Group": "Bark Group",
"Bark Sound": "Bark Sound",
WebHookUrl: "WebHookUrl",
SecretKey: "SecretKey",
"For safety, must use secret key": "В целях безопасности необходимо использовать секретный ключ",
"Device Token": "Токен устройства",
Platform: "Платформа",
iOS: "iOS",
Android: "Android",
Huawei: "Huawei",
High: "High",
Retry: "Повторить",
Topic: "Тема",
"WeCom Bot Key": "WeCom Bot Key",
User: "Пользователь",
Installed: "Установлено",
"Not installed": "Не установлено",
Running: "Запускается",
"Not running": "Не запускается",
"Remove Token": "Удалить токен",
Start: "Запустить",
Stop: "Остановить",
"Uptime Kuma": "Uptime Kuma",
Slug: "Slug",
"Accept characters:": "Принимаемые символы:",
startOrEndWithOnly: "Начинается или кончается только {0}",
"No consecutive dashes": "Без последовательных тире",
"The slug is already taken. Please choose another slug.": "The slug is already taken. Please choose another slug.",
"Page Not Found": "Страница не найдена",
wayToGetCloudflaredURL: "(Скачать cloudflared с {0})",
cloudflareWebsite: "Cloudflare Website",
"Message:": "Сообщение:",
"Don't know how to get the token? Please read the guide:": "Don't know how to get the token? Please read the guide:",
"The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.",
"HTTP Headers": "HTTP заголовки",
"Trust Proxy": "Доверять прокси",
"Other Software": "Другое программное обеспечение",
"For example: nginx, Apache and Traefik.": "К примеру: nginx, Apache и Traefik.",
"Please read": "Пожалуйста, прочитайте",
"Subject:": "Тема:",
"Valid To:": "Действителен до:",
"Days Remaining:": "Дней осталось:",
"Issuer:": "Издатель:",
"Fingerprint:": "Отпечаток:",
"No status pages": "Нет статусных страниц",
"Domain Name Expiry Notification": "Уведомление об истечении срока действия доменного имени",
Proxy: "Прокси",
"Date Created": "Дата создания",
HomeAssistant: "Home Assistant",
onebotHttpAddress: "OneBot HTTP Address",
onebotMessageType: "OneBot Message Type",
onebotGroupMessage: "Группа",
onebotPrivateMessage: "Private",
onebotUserOrGroupId: "Группа/ID пользователя",
onebotSafetyTips: "В целях безопасности необходимо установить токен доступа",
"PushDeer Key": "PushDeer Key",
"Footer Text": "Текст нижнего колонтитула",
"Show Powered By": "Показывать на чем создано",
"Domain Names": "Доменные имена",
signedInDisp: "Вы вошли как {0}",
signedInDispDisabled: "Аутентификация отключена.",
RadiusSecret: "Секрет Radius",
RadiusSecretDescription: "Общий секрет между клиентом и сервером",
RadiusCalledStationId: "Идентификатор вызываемой станции",
RadiusCalledStationIdDescription: "Идентификатор вызываемого устройства",
RadiusCallingStationId: "Идентификатор вызывающей станции",
RadiusCallingStationIdDescription: "Идентификатор вызывающего устройства",
"Certificate Expiry Notification": "Уведомление об истечении срока действия сертификата",
"API Username": "Имя пользователя API",
"API Key": "API ключ",
"Recipient Number": "Номер получателя",
"From Name/Number": "Имя/номер отправителя",
"Leave blank to use a shared sender number.": "Оставьте пустым, чтобы использовать общий номер отправителя.",
"Octopush API Version": "Версия API Octopush",
"Legacy Octopush-DM": "Legacy Octopush-DM",
endpoint: "endpoint",
octopushAPIKey: "\"API key\" из учетных данных HTTP API в панели управления",
octopushLogin: "\"Login\" из учетных данных HTTP API в панели управления",
promosmsLogin: "Логин API",
promosmsPassword: "Пароль API",
"pushoversounds pushover": "Pushover (default)",
"pushoversounds bike": "Bike",
"pushoversounds bugle": "Bugle",
"pushoversounds cashregister": "Cash Register",
"pushoversounds classical": "Classical",
"pushoversounds cosmic": "Cosmic",
"pushoversounds falling": "Falling",
"pushoversounds gamelan": "Gamelan",
"pushoversounds incoming": "Incoming",
"pushoversounds intermission": "Intermission",
"pushoversounds magic": "Magic",
"pushoversounds mechanical": "Mechanical",
"pushoversounds pianobar": "Piano Bar",
"pushoversounds siren": "Siren",
"pushoversounds spacealarm": "Space Alarm",
"pushoversounds tugboat": "Tug Boat",
"pushoversounds alien": "Alien Alarm (long)",
"pushoversounds climb": "Climb (long)",
"pushoversounds persistent": "Persistent (long)",
"pushoversounds echo": "Pushover Echo (long)",
"pushoversounds updown": "Up Down (long)",
"pushoversounds vibrate": "Vibrate Only",
"pushoversounds none": "None (silent)",
pushyAPIKey: "Secret API Key",
pushyToken: "Токен устройства",
"Using a Reverse Proxy?": "Используете обратный прокси?",
"Check how to config it for WebSocket": "Проверьте, как настроить его для WebSocket",
"Steam Game Server": "Steam Game Server",
"Most likely causes:": "Наиболее вероятные причины:",
"The resource is no longer available.": "Ресурс больше не доступен.",
"There might be a typing error in the address.": "В адресе может быть опечатка.",
"What you can try:": "Что вы можете попробовать:",
"Retype the address.": "Повторите адрес.",
"Go back to the previous page.": "Вернуться на предыдущую страницу.",
"Coming Soon": "Скоро",
wayToGetClickSendSMSToken: "Вы можете получить имя пользователя API и ключ API из {0} .",
"Connection String": "Строка подключения",
Query: "Запрос",
settingsCertificateExpiry: "Истекание TLS сертификата",
certificationExpiryDescription: "HTTPS Мониторы инициируют уведомление, когда срок действия сертификата TLS истечет:",
"Setup Docker Host": "Настроить Docker Host",
"Connection Type": "Тип соединения",
"Docker Daemon": "Docker Daemon",
deleteDockerHostMsg: "Are you sure want to delete this docker host for all monitors?",
socket: "Socket",
tcp: "TCP / HTTP",
"Docker Container": "Docker контейнер",
"Container Name / ID": "Название контейнера / ID",
"Docker Host": "Docker Host",
"Docker Hosts": "Docker Hosts",
"ntfy Topic": "ntfy Topic",
Domain: "Домен",
Workstation: "Workstation",
disableCloudflaredNoAuthMsg: "Вы находитесь в режиме без авторизации, пароль не требуется.",
trustProxyDescription: "Доверять заголовкам 'X-Forwarded-*'. Если вы хотите получить правильный IP-адрес клиента, а ваш Uptime Kuma находится под Nginx или Apache, вам следует включить этот параметр.",
wayToGetLineNotifyToken: "Вы можете получить токен доступа в {0}",
Examples: "Примеры",
"Home Assistant URL": "Home Assistant URL",
"Long-Lived Access Token": "Токен доступа с длительным сроком службы",
"Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ",
"Notification Service": "Служба уведомлений",
"default: notify all devices": "по стандарту: уведомлять все устройства",
"A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.": "A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.",
"Automations can optionally be triggered in Home Assistant:": "При желании автоматизацию можно активировать в Home Assistant.:",
"Trigger type:": "Тип триггера:",
"Event type:": "Тип события:",
"Event data:": "Данные события:",
"Then choose an action, for example switch the scene to where an RGB light is red.": "Затем выберите действие, например, переключите сцену на красный индикатор RGB..",
"Frontend Version": "Версия интерфейса",
"Frontend Version do not match backend version!": "Версия интерфейса не соответствует версии серверной части!",
"Base URL": "Базовый URL",
goAlertInfo: "GoAlert is a An open source application for on-call scheduling, automated escalations and notifications (like SMS or voice calls). Automatically engage the right person, the right way, and at the right time! {0}",
goAlertIntegrationKeyInfo: "Получить общий ключ интеграции API для сервиса в этом формате \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" обычно значение параметра токена скопированного URL.",
goAlert: "GoAlert",
backupOutdatedWarning: "Устарело: поскольку добавлено множество функций, а эта функция резервного копирования немного не поддерживается, она не может создать или восстановить полную резервную копию.",
backupRecommend: "Сделайте резервную копию тома или папки с данными (./data/) напрямую.",
"Optional": "Необязательно",
squadcast: "Squadcast",
SendKey: "SendKey",
"SMSManager API Docs": "Документация к API SMSManager ",
"Gateway Type": "Тип шлюза",
SMSManager: "SMSManager",
"You can divide numbers with": "Вы можете делить числа с",
"or": "или",
}; };

View file

@ -224,7 +224,7 @@ export default {
teams: "Microsoft Teams", teams: "Microsoft Teams",
"Webhook URL": "Webhook URL", "Webhook URL": "Webhook URL",
wayToGetTeamsURL: "Bir webhook URL'sinin nasıl oluşturulacağını öğrenebilirsiniz {0}.", wayToGetTeamsURL: "Bir webhook URL'sinin nasıl oluşturulacağını öğrenebilirsiniz {0}.",
signal: "Signal", signal: "Sinyal",
Number: "Numara", Number: "Numara",
Recipients: "Alıcılar", Recipients: "Alıcılar",
needSignalAPI: "REST API ile bir signal istemciniz olması gerekiyor.", needSignalAPI: "REST API ile bir signal istemciniz olması gerekiyor.",
@ -552,14 +552,14 @@ export default {
deleteDockerHostMsg: "Bu docker ana bilgisayarını tüm monitörler için silmek istediğinizden emin misiniz?", deleteDockerHostMsg: "Bu docker ana bilgisayarını tüm monitörler için silmek istediğinizden emin misiniz?",
socket: "Soket", socket: "Soket",
tcp: "TCP / HTTP", tcp: "TCP / HTTP",
"Docker Container": "Docker Konteyneri", "Docker Container": "Docker Konteyner",
"Container Name / ID": "Konteyner Adı / Kimliği", "Container Name / ID": "Konteyner Adı / Kimliği",
"Docker Host": "Docker Ana Bilgisayarı", "Docker Host": "Docker Ana Bilgisayarı",
"Docker Hosts": "Docker Ana Bilgisayarları", "Docker Hosts": "Docker Ana Bilgisayarları",
"ntfy Topic": "ntfy Konu", "ntfy Topic": "ntfy Konu",
Domain: "Domain", Domain: "Domain",
Workstation: "İş İstasyonu", Workstation: "İş İstasyonu",
disableCloudflaredNoAuthMsg: "Yetki Yok modundasınız, şifre gerekli değil.", disableCloudflaredNoAuthMsg: "Yetki yok modundasınız, şifre gerekli değil.",
trustProxyDescription: "'X-Forwarded-*' başlıklarına güvenin. Doğru istemci IP'sini almak istiyorsanız ve Uptime Kuma'nız Nginx veya Apache'nin arkasındaysa, bunu etkinleştirmelisiniz.", trustProxyDescription: "'X-Forwarded-*' başlıklarına güvenin. Doğru istemci IP'sini almak istiyorsanız ve Uptime Kuma'nız Nginx veya Apache'nin arkasındaysa, bunu etkinleştirmelisiniz.",
wayToGetLineNotifyToken: "{0} adresinden bir erişim jetonu alabilirsiniz.", wayToGetLineNotifyToken: "{0} adresinden bir erişim jetonu alabilirsiniz.",
Examples: "Örnekler", Examples: "Örnekler",

View file

@ -581,4 +581,18 @@ export default {
"Then choose an action, for example switch the scene to where an RGB light is red.": "然后您可以选择关联操作,例如切换到 RGB 灯发出红光的场景", "Then choose an action, for example switch the scene to where an RGB light is red.": "然后您可以选择关联操作,例如切换到 RGB 灯发出红光的场景",
"Frontend Version": "前端版本", "Frontend Version": "前端版本",
"Frontend Version do not match backend version!": "前端版本与后端版本不符!", "Frontend Version do not match backend version!": "前端版本与后端版本不符!",
"Base URL": "API 基础地址",
goAlertInfo: "GoAlert 是一个用于呼叫调度、自动汇报和通知(如 SMS 或语音呼叫)的开源应用程序。在正确的时间以正确的方式自动让正确的人参与!{0}",
goAlertIntegrationKeyInfo: "使用形如 aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee 的通用 API 集成密钥,通常是复制来的链接中的 token 参数值。",
goAlert: "GoAlert",
backupOutdatedWarning: "已弃用:由于大量新功能的加入,以及备份功能没有时时维护,现在备份功能已经无法生成完整的备份和恢复完整的设置。",
backupRecommend: "请改为直接备份 docker 卷或者数据文件夹(./data/)。",
Optional: "可选的",
squadcast: "Squadcast",
SendKey: "SendKey",
"SMSManager API Docs": "SMSManager API 文档在",
"Gateway Type": "网关类型",
SMSManager: "SMSManager",
"You can divide numbers with": "可用的分隔符:",
"or": "或",
}; };

View file

@ -380,4 +380,6 @@ export default {
proxyDescription: "必須將代理伺服器指派給監測器才能運作。", proxyDescription: "必須將代理伺服器指派給監測器才能運作。",
enableProxyDescription: "此代理伺服器在啟用前不會在監測器上生效,您可以藉由控制啟用狀態來暫時對所有的監測器停用代理伺服器。", enableProxyDescription: "此代理伺服器在啟用前不會在監測器上生效,您可以藉由控制啟用狀態來暫時對所有的監測器停用代理伺服器。",
setAsDefaultProxyDescription: "預設情況下,新監測器將啟用此代理伺服器。您仍可分別停用各監測器的代理伺服器。", setAsDefaultProxyDescription: "預設情況下,新監測器將啟用此代理伺服器。您仍可分別停用各監測器的代理伺服器。",
Maintenance: "維護",
statusMaintenance: "維護中",
}; };

View file

@ -2,16 +2,31 @@ export default {
languageName: "繁體中文 (台灣)", languageName: "繁體中文 (台灣)",
checkEverySecond: "每 {0} 秒檢查一次", checkEverySecond: "每 {0} 秒檢查一次",
retryCheckEverySecond: "每 {0} 秒重試一次", retryCheckEverySecond: "每 {0} 秒重試一次",
resendEveryXTimes: "每 {0} 次便重新傳送",
resendDisabled: "重新傳送已停用",
retriesDescription: "在服務被標記為離線並傳送通知前的最大重試次數", retriesDescription: "在服務被標記為離線並傳送通知前的最大重試次數",
ignoreTLSError: "忽略 HTTPS 網站的 TLS/SSL 錯誤", ignoreTLSError: "忽略 HTTPS 網站的 TLS/SSL 錯誤",
upsideDownModeDescription: "反轉顯示狀態。若服務可以連線,將顯示離線。", upsideDownModeDescription: "反轉顯示狀態。若服務可以連線,將顯示離線。",
maxRedirectDescription: "最大重新導向跟隨次數。設為 0 將停用重新導向。", maxRedirectDescription: "最大重新導向跟隨次數。設為 0 將停用重新導向。",
acceptedStatusCodesDescription: "選擇視為成功回應的狀態碼。", acceptedStatusCodesDescription: "選擇視為成功回應的狀態碼。",
Maintenance: "維護",
statusMaintenance: "維護",
"Schedule maintenance": "排程維護",
"Affected Monitors": "受影響的監測器",
"Pick Affected Monitors...": "挑選受影響的監測器...",
"Start of maintenance": "維護起始",
"All Status Pages": "所有狀態頁",
"Select status pages...": "選擇狀態頁...",
recurringIntervalMessage: "每日執行 | 每 {0} 天執行",
affectedMonitorsDescription: "選擇受目前維護影響的監測器",
affectedStatusPages: "在已選取的狀態頁中顯示此維護訊息",
atLeastOneMonitor: "至少選擇一個受影響的監測器",
passwordNotMatchMsg: "密碼不相符。", passwordNotMatchMsg: "密碼不相符。",
notificationDescription: "必須將通知指派給監測器才能運作。", notificationDescription: "必須將通知指派給監測器才能運作。",
keywordDescription: "HTML 或 JSON 回應的搜尋關鍵字。區分大小寫。", keywordDescription: "HTML 或 JSON 回應的搜尋關鍵字。區分大小寫。",
pauseDashboardHome: "暫停", pauseDashboardHome: "暫停",
deleteMonitorMsg: "您確定要刪除此監測器嗎?", deleteMonitorMsg: "您確定要刪除此監測器嗎?",
deleteMaintenanceMsg: "您確定要刪除此維護嗎?",
deleteNotificationMsg: "您確定要為所有監測器刪除此通知嗎?", deleteNotificationMsg: "您確定要為所有監測器刪除此通知嗎?",
dnsPortDescription: "DNS 伺服器連接埠。預設為 53。您可以隨時變更連接埠。", dnsPortDescription: "DNS 伺服器連接埠。預設為 53。您可以隨時變更連接埠。",
resolverserverDescription: "Cloudflare 為預設伺服器。您可以隨時更換解析伺服器。", resolverserverDescription: "Cloudflare 為預設伺服器。您可以隨時更換解析伺服器。",
@ -72,6 +87,7 @@ export default {
"Heartbeat Interval": "心跳間隔", "Heartbeat Interval": "心跳間隔",
Retries: "重試次數", Retries: "重試次數",
"Heartbeat Retry Interval": "心跳重試間隔", "Heartbeat Retry Interval": "心跳重試間隔",
"Resend Notification if Down X times consequently": "若 X 次心跳皆離線,重新傳送通知",
Advanced: "進階", Advanced: "進階",
"Upside Down Mode": "顛倒模式", "Upside Down Mode": "顛倒模式",
"Max. Redirects": "最大重新導向次數", "Max. Redirects": "最大重新導向次數",
@ -302,7 +318,7 @@ export default {
Method: "方法", Method: "方法",
Body: "主體", Body: "主體",
Headers: "標頭", Headers: "標頭",
PushUrl: "Push URL", PushUrl: "Push 網址",
HeadersInvalidFormat: "要求標頭不是有效的 JSON", HeadersInvalidFormat: "要求標頭不是有效的 JSON",
BodyInvalidFormat: "請求主體不是有效的 JSON", BodyInvalidFormat: "請求主體不是有效的 JSON",
"Monitor History": "監測器歷史紀錄", "Monitor History": "監測器歷史紀錄",
@ -455,6 +471,8 @@ export default {
"Message:": "訊息:", "Message:": "訊息:",
"Don't know how to get the token? Please read the guide:": "不知道如何取得權杖嗎?請閱讀指南:", "Don't know how to get the token? Please read the guide:": "不知道如何取得權杖嗎?請閱讀指南:",
"The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "如果您目前正透過 Cloudflare Tunnel 連線,可能會導致連線中斷。您確定要停止嗎?請輸入密碼以確認。", "The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "如果您目前正透過 Cloudflare Tunnel 連線,可能會導致連線中斷。您確定要停止嗎?請輸入密碼以確認。",
"HTTP Headers": "HTTP 標頭",
"Trust Proxy": "信任的 Proxy",
"Other Software": "其他軟體", "Other Software": "其他軟體",
"For example: nginx, Apache and Traefik.": "例如 nginx、Apache 和 Traefik。", "For example: nginx, Apache and Traefik.": "例如 nginx、Apache 和 Traefik。",
"Please read": "請閱覽", "Please read": "請閱覽",
@ -467,6 +485,7 @@ export default {
"Domain Name Expiry Notification": "網域名稱到期通知", "Domain Name Expiry Notification": "網域名稱到期通知",
Proxy: "Proxy", Proxy: "Proxy",
"Date Created": "建立日期", "Date Created": "建立日期",
HomeAssistant: "Home Assistant",
onebotHttpAddress: "OneBot HTTP 位址", onebotHttpAddress: "OneBot HTTP 位址",
onebotMessageType: "OneBot 訊息類型", onebotMessageType: "OneBot 訊息類型",
onebotGroupMessage: "群組", onebotGroupMessage: "群組",
@ -479,6 +498,12 @@ export default {
"Domain Names": "網域名稱", "Domain Names": "網域名稱",
signedInDisp: "以 {0} 身分登入", signedInDisp: "以 {0} 身分登入",
signedInDispDisabled: "驗證已停用。", signedInDispDisabled: "驗證已停用。",
RadiusSecret: "Radius Secret",
RadiusSecretDescription: "客戶端與伺服器端的共享機密",
RadiusCalledStationId: "被叫站 Id",
RadiusCalledStationIdDescription: "被呼叫裝置的識別碼",
RadiusCallingStationId: "呼叫站 Id",
RadiusCallingStationIdDescription: "呼叫裝置的識別碼",
"Certificate Expiry Notification": "憑證到期通知", "Certificate Expiry Notification": "憑證到期通知",
"API Username": "API 使用者名稱", "API Username": "API 使用者名稱",
"API Key": "API 金鑰", "API Key": "API 金鑰",
@ -488,8 +513,8 @@ export default {
"Octopush API Version": "Octopush API 版本", "Octopush API Version": "Octopush API 版本",
"Legacy Octopush-DM": "舊版 Octopush-DM", "Legacy Octopush-DM": "舊版 Octopush-DM",
"endpoint": "端", "endpoint": "端",
octopushAPIKey: "\"API key\" from HTTP API credentials in control panel", octopushAPIKey: "在控制台的 HTTP API 憑證取得的 \"API 金鑰\"",
octopushLogin: "\"Login\" from HTTP API credentials in control panel", octopushLogin: "在控制台的 HTTP API 憑證取得的 \"Login\"",
promosmsLogin: "API 登入名稱", promosmsLogin: "API 登入名稱",
promosmsPassword: "API 密碼", promosmsPassword: "API 密碼",
"pushoversounds pushover": "Pushover (預設)", "pushoversounds pushover": "Pushover (預設)",
@ -504,9 +529,9 @@ export default {
"pushoversounds intermission": "中場休息", "pushoversounds intermission": "中場休息",
"pushoversounds magic": "魔法", "pushoversounds magic": "魔法",
"pushoversounds mechanical": "機械", "pushoversounds mechanical": "機械",
"pushoversounds pianobar": "Piano Bar", "pushoversounds pianobar": "鋼琴酒吧",
"pushoversounds siren": "Siren", "pushoversounds siren": "警鈴",
"pushoversounds spacealarm": "Space Alarm", "pushoversounds spacealarm": "太空鬧鐘",
"pushoversounds tugboat": "汽笛", "pushoversounds tugboat": "汽笛",
"pushoversounds alien": "外星鬧鐘 (長)", "pushoversounds alien": "外星鬧鐘 (長)",
"pushoversounds climb": "爬升 (長)", "pushoversounds climb": "爬升 (長)",
@ -531,11 +556,79 @@ export default {
"Coming Soon": "即將推出", "Coming Soon": "即將推出",
wayToGetClickSendSMSToken: "您可以從 {0} 取得 API 使用者名稱和金鑰。", wayToGetClickSendSMSToken: "您可以從 {0} 取得 API 使用者名稱和金鑰。",
"Connection String": "連線字串", "Connection String": "連線字串",
"Query": "查詢", Query: "查詢",
settingsCertificateExpiry: "TLS 憑證到期", settingsCertificateExpiry: "TLS 憑證到期",
certificationExpiryDescription: "TLS 將於 X 天後到期時觸發 HTTPS 監測器通知:", certificationExpiryDescription: "TLS 將於 X 天後到期時觸發 HTTPS 監測器通知:",
"Setup Docker Host": "設定 Docker 主機",
"Connection Type": "連線類型",
"Docker Daemon": "Docker 精靈",
deleteDockerHostMsg: "您確定要為所有監測器刪除此 Docker 主機嗎?",
socket: "通訊端",
tcp: "TCP / HTTP",
"Docker Container": "Docker 容器",
"Container Name / ID": "容器名稱 / ID",
"Docker Host": "Docker 主機",
"Docker Hosts": "Docker 主機",
"ntfy Topic": "ntfy 主題", "ntfy Topic": "ntfy 主題",
"Domain": "網域", Domain: "網域",
"Workstation": "工作站", Workstation: "工作站",
disableCloudflaredNoAuthMsg: "您處於無驗證模式。無須輸入密碼。", disableCloudflaredNoAuthMsg: "您處於無驗證模式。無須輸入密碼。",
trustProxyDescription: "信任 'X-Forwarded-*' 標頭。如果您想要取得正確的客戶端 IP且您的 Uptime Kuma 架設於 Nginx 或 Apache 後方,您應啟用此選項。",
wayToGetLineNotifyToken: "您可以從 {0} 取得存取權杖",
Examples: "範例",
"Home Assistant URL": "Home Assistant 網址",
"Long-Lived Access Token": "長期有效存取權杖",
"Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "若要建立長期有效存取權杖,請點擊您的個人檔案名稱 (左下角),捲動至最下方,然後點擊建立權杖。",
"Notification Service": "通知服務",
"default: notify all devices": "預設:通知所有服務",
"A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.": "您可以在 Home Assistant 中查看通知服務的列表,在\"開發者工具 > 服務\"下搜尋\"通知\"來找到您的裝置/手機的名稱。",
"Automations can optionally be triggered in Home Assistant:": "可以選擇在 Home Assistant 中觸發自動化程序:",
"Trigger type:": "觸發器類型:",
"Event type:": "事件類型:",
"Event data:": "事件資料:",
"Then choose an action, for example switch the scene to where an RGB light is red.": "然後選擇動作,例如切換至 RGB 燈為紅色的場景。",
"Frontend Version": "前端版本",
"Frontend Version do not match backend version!": "前端版本與後端版本不符!",
"Base URL": "基底網址",
goAlertInfo: "GoAlert 是用於待命排程、升級自動化,以及通知 (如簡訊或語音通話) 的開源應用程式。自動在正確的時間、用洽當的方法、聯絡合適的人! {0}",
goAlertIntegrationKeyInfo: "取得服務的通用 API 整合金鑰,格式為 \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\"。通常是已複製的網址的權杖參數值。",
goAlert: "GoAlert",
backupOutdatedWarning: "過時:由於新功能的增加,且未妥善維護,故此備份功能無法產生或復原完整備份。",
backupRecommend: "請直接備份磁碟區或 ./data/ 資料夾。",
"Optional": "選填",
squadcast: "Squadcast",
SendKey: "SendKey",
"SMSManager API Docs": "SMSManager API 文件 ",
"Gateway Type": "閘道類型",
SMSManager: "SMSManager",
"You can divide numbers with": "若要除數,您可以使用",
"or": "或是",
recurringInterval: "間隔",
"Recurring": "週期性",
strategyManual: "手動切換使用中/非使用中",
warningTimezone: "正在使用伺服器的時區",
weekdayShortMon: "一",
weekdayShortTue: "二",
weekdayShortWed: "三",
weekdayShortThu: "四",
weekdayShortFri: "五",
weekdayShortSat: "六",
weekdayShortSun: "日",
dayOfWeek: "每周特定一天",
dayOfMonth: "每月特定一天",
lastDay: "最後一天",
lastDay1: "每月的最後一天",
lastDay2: "每月的倒數第二天",
lastDay3: "每月的倒數第三天",
lastDay4: "每月的倒數第四天",
"No Maintenance": "無維護",
pauseMaintenanceMsg: "您確定要暫停嗎?",
"maintenanceStatus-under-maintenance": "維護中",
"maintenanceStatus-inactive": "非使用中",
"maintenanceStatus-scheduled": "已排程",
"maintenanceStatus-ended": "已結束",
"maintenanceStatus-unknown": "未知",
"Display Timezone": "顯示時區",
"Server Timezone": "伺服器時區",
statusPageMaintenanceEndDate: "結束",
}; };

View file

@ -37,19 +37,32 @@
<div class="profile-pic">{{ $root.usernameFirstChar }}</div> <div class="profile-pic">{{ $root.usernameFirstChar }}</div>
<font-awesome-icon icon="angle-down" /> <font-awesome-icon icon="angle-down" />
</div> </div>
<!-- Header's Dropdown Menu -->
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<!-- Username -->
<li> <li>
<i18n-t v-if="$root.username != null" tag="span" keypath="signedInDisp" class="dropdown-item-text"> <i18n-t v-if="$root.username != null" tag="span" keypath="signedInDisp" class="dropdown-item-text">
<strong>{{ $root.username }}</strong> <strong>{{ $root.username }}</strong>
</i18n-t> </i18n-t>
<span v-if="$root.username == null" class="dropdown-item-text">{{ $t("signedInDispDisabled") }}</span> <span v-if="$root.username == null" class="dropdown-item-text">{{ $t("signedInDispDisabled") }}</span>
</li> </li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<!-- Functions -->
<li> <li>
<router-link to="/settings" class="dropdown-item" :class="{ active: $route.path.includes('settings') }"> <router-link to="/maintenance" class="dropdown-item" :class="{ active: $route.path.includes('manage-maintenance') }">
<font-awesome-icon icon="wrench" /> {{ $t("Maintenance") }}
</router-link>
</li>
<li>
<router-link to="/settings/general" class="dropdown-item" :class="{ active: $route.path.includes('settings') }">
<font-awesome-icon icon="cog" /> {{ $t("Settings") }} <font-awesome-icon icon="cog" /> {{ $t("Settings") }}
</router-link> </router-link>
</li> </li>
<li v-if="$root.loggedIn && $root.socket.token !== 'autoLogin'"> <li v-if="$root.loggedIn && $root.socket.token !== 'autoLogin'">
<button class="dropdown-item" @click="$root.logout"> <button class="dropdown-item" @click="$root.logout">
<font-awesome-icon icon="sign-out-alt" /> <font-awesome-icon icon="sign-out-alt" />
@ -304,5 +317,4 @@ main {
background-color: $dark-bg; background-color: $dark-bg;
} }
} }
</style> </style>

View file

@ -5,6 +5,7 @@ import Toast from "vue-toastification";
import "vue-toastification/dist/index.css"; import "vue-toastification/dist/index.css";
import App from "./App.vue"; import App from "./App.vue";
import "./assets/app.scss"; import "./assets/app.scss";
import "./assets/vue-datepicker.scss";
import { i18n } from "./i18n"; import { i18n } from "./i18n";
import { FontAwesomeIcon } from "./icon.js"; import { FontAwesomeIcon } from "./icon.js";
import datetime from "./mixins/datetime"; import datetime from "./mixins/datetime";
@ -15,6 +16,13 @@ import theme from "./mixins/theme";
import lang from "./mixins/lang"; import lang from "./mixins/lang";
import { router } from "./router"; import { router } from "./router";
import { appName } from "./util.ts"; import { appName } from "./util.ts";
import dayjs from "dayjs";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
import relativeTime from "dayjs/plugin/relativeTime";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(relativeTime);
const app = createApp({ const app = createApp({
mixins: [ mixins: [

View file

@ -1,10 +1,4 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(relativeTime);
/** /**
* DateTime Mixin * DateTime Mixin
@ -18,6 +12,19 @@ export default {
}, },
methods: { methods: {
toUTC(value) {
return dayjs.tz(value, this.timezone).utc().format();
},
/**
* Used for <input type="datetime" />
* @param value
* @returns {string}
*/
toDateTimeInputFormat(value) {
return this.datetimeFormat(value, "YYYY-MM-DDTHH:mm");
},
/** /**
* Return a given value in the format YYYY-MM-DD HH:mm:ss * Return a given value in the format YYYY-MM-DD HH:mm:ss
* @param {any} value Value to format as date time * @param {any} value Value to format as date time
@ -27,6 +34,17 @@ export default {
return this.datetimeFormat(value, "YYYY-MM-DD HH:mm:ss"); return this.datetimeFormat(value, "YYYY-MM-DD HH:mm:ss");
}, },
datetimeMaintenance(value) {
const inputDate = new Date(value);
const now = new Date(Date.now());
if (inputDate.getFullYear() === now.getUTCFullYear() && inputDate.getMonth() === now.getUTCMonth() && inputDate.getDay() === now.getUTCDay()) {
return this.datetimeFormat(value, "HH:mm");
} else {
return this.datetimeFormat(value, "YYYY-MM-DD HH:mm");
}
},
/** /**
* Return a given value in the format YYYY-MM-DD * Return a given value in the format YYYY-MM-DD
* @param {any} value Value to format as date * @param {any} value Value to format as date
@ -64,7 +82,7 @@ export default {
return dayjs.utc(value).tz(this.timezone).format(format); return dayjs.utc(value).tz(this.timezone).format(format);
} }
return ""; return "";
} },
}, },
computed: { computed: {

View file

@ -33,6 +33,7 @@ export default {
allowLoginDialog: false, // Allowed to show login dialog, but "loggedIn" have to be true too. This exists because prevent the login dialog show 0.1s in first before the socket server auth-ed. allowLoginDialog: false, // Allowed to show login dialog, but "loggedIn" have to be true too. This exists because prevent the login dialog show 0.1s in first before the socket server auth-ed.
loggedIn: false, loggedIn: false,
monitorList: { }, monitorList: { },
maintenanceList: { },
heartbeatList: { }, heartbeatList: { },
importantHeartbeatList: { }, importantHeartbeatList: { },
avgPingList: { }, avgPingList: { },
@ -57,7 +58,6 @@ export default {
}, },
created() { created() {
window.addEventListener("resize", this.onResize);
this.initSocketIO(); this.initSocketIO();
}, },
@ -129,6 +129,10 @@ export default {
this.monitorList = data; this.monitorList = data;
}); });
socket.on("maintenanceList", (data) => {
this.maintenanceList = data;
});
socket.on("notificationList", (data) => { socket.on("notificationList", (data) => {
this.notificationList = data; this.notificationList = data;
}); });
@ -445,6 +449,13 @@ export default {
socket.emit("getMonitorList", callback); socket.emit("getMonitorList", callback);
}, },
getMaintenanceList(callback) {
if (! callback) {
callback = () => { };
}
socket.emit("getMaintenanceList", callback);
},
/** /**
* Add a monitor * Add a monitor
* @param {Object} monitor Object representing monitor to add * @param {Object} monitor Object representing monitor to add
@ -454,6 +465,26 @@ export default {
socket.emit("add", monitor, callback); socket.emit("add", monitor, callback);
}, },
addMaintenance(maintenance, callback) {
socket.emit("addMaintenance", maintenance, callback);
},
addMonitorMaintenance(maintenanceID, monitors, callback) {
socket.emit("addMonitorMaintenance", maintenanceID, monitors, callback);
},
addMaintenanceStatusPage(maintenanceID, statusPages, callback) {
socket.emit("addMaintenanceStatusPage", maintenanceID, statusPages, callback);
},
getMonitorMaintenance(maintenanceID, callback) {
socket.emit("getMonitorMaintenance", maintenanceID, callback);
},
getMaintenanceStatusPage(maintenanceID, callback) {
socket.emit("getMaintenanceStatusPage", maintenanceID, callback);
},
/** /**
* Delete monitor by ID * Delete monitor by ID
* @param {number} monitorID ID of monitor to delete * @param {number} monitorID ID of monitor to delete
@ -463,6 +494,10 @@ export default {
socket.emit("deleteMonitor", monitorID, callback); socket.emit("deleteMonitor", monitorID, callback);
}, },
deleteMaintenance(maintenanceID, callback) {
socket.emit("deleteMaintenance", maintenanceID, callback);
},
/** Clear the hearbeat list */ /** Clear the hearbeat list */
clearData() { clearData() {
console.log("reset heartbeat list"); console.log("reset heartbeat list");
@ -550,7 +585,12 @@ export default {
for (let monitorID in this.lastHeartbeatList) { for (let monitorID in this.lastHeartbeatList) {
let lastHeartBeat = this.lastHeartbeatList[monitorID]; let lastHeartBeat = this.lastHeartbeatList[monitorID];
if (! lastHeartBeat) { if (this.monitorList[monitorID].maintenance) {
result[monitorID] = {
text: this.$t("statusMaintenance"),
color: "maintenance",
};
} else if (! lastHeartBeat) {
result[monitorID] = unknown; result[monitorID] = unknown;
} else if (lastHeartBeat.status === 1) { } else if (lastHeartBeat.status === 1) {
result[monitorID] = { result[monitorID] = {
@ -579,6 +619,7 @@ export default {
let result = { let result = {
up: 0, up: 0,
down: 0, down: 0,
maintenance: 0,
unknown: 0, unknown: 0,
pause: 0, pause: 0,
}; };
@ -587,7 +628,9 @@ export default {
let beat = this.$root.lastHeartbeatList[monitorID]; let beat = this.$root.lastHeartbeatList[monitorID];
let monitor = this.$root.monitorList[monitorID]; let monitor = this.$root.monitorList[monitorID];
if (monitor && ! monitor.active) { if (monitor && monitor.maintenance) {
result.maintenance++;
} else if (monitor && ! monitor.active) {
result.pause++; result.pause++;
} else if (beat) { } else if (beat) {
if (beat.status === 1) { if (beat.status === 1) {

View file

@ -46,6 +46,10 @@ export default {
} }
return this.userTheme; return this.userTheme;
} }
},
isDark() {
return this.theme === "dark";
} }
}, },

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div v-if="! $root.isMobile" class="col-12 col-md-5 col-xl-4"> <div v-if="!$root.isMobile" class="col-12 col-md-5 col-xl-4">
<div> <div>
<router-link to="/add" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> {{ $t("Add New Monitor") }}</router-link> <router-link to="/add" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> {{ $t("Add New Monitor") }}</router-link>
</div> </div>

View file

@ -15,6 +15,10 @@
<h3>{{ $t("Down") }}</h3> <h3>{{ $t("Down") }}</h3>
<span class="num text-danger">{{ $root.stats.down }}</span> <span class="num text-danger">{{ $root.stats.down }}</span>
</div> </div>
<div class="col">
<h3>{{ $t("Maintenance") }}</h3>
<span class="num text-maintenance">{{ $root.stats.maintenance }}</span>
</div>
<div class="col"> <div class="col">
<h3>{{ $t("Unknown") }}</h3> <h3>{{ $t("Unknown") }}</h3>
<span class="num text-secondary">{{ $root.stats.unknown }}</span> <span class="num text-secondary">{{ $root.stats.unknown }}</span>

View file

@ -20,19 +20,21 @@
</p> </p>
<div class="functions"> <div class="functions">
<button v-if="monitor.active" class="btn btn-light" @click="pauseDialog"> <div class="btn-group" role="group">
<button v-if="monitor.active" class="btn btn-normal" @click="pauseDialog">
<font-awesome-icon icon="pause" /> {{ $t("Pause") }} <font-awesome-icon icon="pause" /> {{ $t("Pause") }}
</button> </button>
<button v-if="! monitor.active" class="btn btn-primary" @click="resumeMonitor"> <button v-if="! monitor.active" class="btn btn-primary" @click="resumeMonitor">
<font-awesome-icon icon="play" /> {{ $t("Resume") }} <font-awesome-icon icon="play" /> {{ $t("Resume") }}
</button> </button>
<router-link :to=" '/edit/' + monitor.id " class="btn btn-secondary"> <router-link :to=" '/edit/' + monitor.id " class="btn btn-normal">
<font-awesome-icon icon="edit" /> {{ $t("Edit") }} <font-awesome-icon icon="edit" /> {{ $t("Edit") }}
</router-link> </router-link>
<button class="btn btn-danger" @click="deleteDialog"> <button class="btn btn-danger" @click="deleteDialog">
<font-awesome-icon icon="trash" /> {{ $t("Delete") }} <font-awesome-icon icon="trash" /> {{ $t("Delete") }}
</button> </button>
</div> </div>
</div>
<div class="shadow-box"> <div class="shadow-box">
<div class="row"> <div class="row">
@ -392,11 +394,6 @@ export default {
@media (max-width: 550px) { @media (max-width: 550px) {
.functions { .functions {
text-align: center; text-align: center;
button, a {
margin-left: 10px !important;
margin-right: 10px !important;
}
} }
.ping-chart-wrapper { .ping-chart-wrapper {
@ -439,12 +436,6 @@ export default {
} }
} }
.functions {
button, a {
margin-right: 20px;
}
}
.shadow-box { .shadow-box {
padding: 20px; padding: 20px;
margin-top: 25px; margin-top: 25px;

View file

@ -0,0 +1,534 @@
<template>
<transition name="slide-fade" appear>
<div>
<h1 class="mb-3">{{ pageName }}</h1>
<form @submit.prevent="submit">
<div class="shadow-box">
<div class="row">
<div class="col-xl-10">
<!-- Title -->
<div class="mb-3">
<label for="name" class="form-label">{{ $t("Title") }}</label>
<input
id="name" v-model="maintenance.title" type="text" class="form-control"
required
>
</div>
<!-- Description -->
<div class="my-3">
<label for="description" class="form-label">{{ $t("Description") }}</label>
<textarea
id="description" v-model="maintenance.description" class="form-control"
></textarea>
</div>
<!-- Affected Monitors -->
<h2 class="mt-5">{{ $t("Affected Monitors") }}</h2>
{{ $t("affectedMonitorsDescription") }}
<div class="my-3">
<VueMultiselect
id="affected_monitors"
v-model="affectedMonitors"
:options="affectedMonitorsOptions"
track-by="id"
label="name"
:multiple="true"
:close-on-select="false"
:clear-on-select="false"
:preserve-search="true"
:placeholder="$t('Pick Affected Monitors...')"
:preselect-first="false"
:max-height="600"
:taggable="false"
></VueMultiselect>
</div>
<!-- Status pages to display maintenance info on -->
<h2 class="mt-5">{{ $t("Status Pages") }}</h2>
{{ $t("affectedStatusPages") }}
<div class="my-3">
<!-- Show on all pages -->
<div class="form-check mb-2">
<input
id="show-on-all-pages" v-model="showOnAllPages" class="form-check-input"
type="checkbox"
>
<label class="form-check-label" for="show-powered-by">{{
$t("All Status Pages")
}}</label>
</div>
<div v-if="!showOnAllPages">
<VueMultiselect
id="selected_status_pages"
v-model="selectedStatusPages"
:options="selectedStatusPagesOptions"
track-by="id"
label="name"
:multiple="true"
:close-on-select="false"
:clear-on-select="false"
:preserve-search="true"
:placeholder="$t('Select status pages...')"
:preselect-first="false"
:max-height="600"
:taggable="false"
></VueMultiselect>
</div>
</div>
<h2 class="mt-5">{{ $t("Date and Time") }}</h2>
<div> {{ $t("warningTimezone") }}: <mark>{{ $root.info.serverTimezone }} ({{ $root.info.serverTimezoneOffset }})</mark></div>
<!-- Strategy -->
<div class="my-3">
<label for="strategy" class="form-label">{{ $t("Strategy") }}</label>
<select id="strategy" v-model="maintenance.strategy" class="form-select">
<option value="manual">{{ $t("strategyManual") }}</option>
<option value="single">{{ $t("Single Maintenance Window") }}</option>
<option value="recurring-interval">{{ $t("Recurring") }} - {{ $t("recurringInterval") }}</option>
<option value="recurring-weekday">{{ $t("Recurring") }} - {{ $t("dayOfWeek") }}</option>
<option value="recurring-day-of-month">{{ $t("Recurring") }} - {{ $t("dayOfMonth") }}</option>
<option v-if="false" value="recurring-day-of-year">{{ $t("Recurring") }} - Day of Year</option>
</select>
</div>
<!-- Single Maintenance Window -->
<template v-if="maintenance.strategy === 'single'">
<!-- DateTime Range -->
<div class="my-3">
<label class="form-label">{{ $t("DateTime Range") }}</label>
<Datepicker
v-model="maintenance.dateRange"
:dark="$root.isDark"
range
:monthChangeOnScroll="false"
:minDate="minDate"
format="yyyy-MM-dd HH:mm"
modelType="yyyy-MM-dd HH:mm:ss"
/>
</div>
</template>
<!-- Recurring - Interval -->
<template v-if="maintenance.strategy === 'recurring-interval'">
<div class="my-3">
<label for="interval-day" class="form-label">
{{ $t("recurringInterval") }}
<template v-if="maintenance.intervalDay >= 1">
({{
$tc("recurringIntervalMessage", maintenance.intervalDay, [
maintenance.intervalDay
])
}})
</template>
</label>
<input id="interval-day" v-model="maintenance.intervalDay" type="number" class="form-control" required min="1" max="3650" step="1">
</div>
</template>
<!-- Recurring - Weekday -->
<template v-if="maintenance.strategy === 'recurring-weekday'">
<div class="my-3">
<label for="interval-day" class="form-label">
{{ $t("dayOfWeek") }}
</label>
<!-- Weekday Picker -->
<div class="weekday-picker">
<div v-for="(weekday, index) in weekdays" :key="index">
<label class="form-check-label" :for="weekday.id">{{ $t(weekday.langKey) }}</label>
<div class="form-check-inline"><input :id="weekday.id" v-model="maintenance.weekdays" type="checkbox" :value="weekday.value" class="form-check-input"></div>
</div>
</div>
</div>
</template>
<!-- Recurring - Day of month -->
<template v-if="maintenance.strategy === 'recurring-day-of-month'">
<div class="my-3">
<label for="interval-day" class="form-label">
{{ $t("dayOfMonth") }}
</label>
<!-- Day Picker -->
<div class="day-picker">
<div v-for="index in 31" :key="index">
<label class="form-check-label" :for="'day' + index">{{ index }}</label>
<div class="form-check-inline">
<input :id="'day' + index" v-model="maintenance.daysOfMonth" type="checkbox" :value="index" class="form-check-input">
</div>
</div>
</div>
<div class="mt-3 mb-2">{{ $t("lastDay") }}</div>
<div v-for="(lastDay, index) in lastDays" :key="index" class="form-check">
<input :id="lastDay.langKey" v-model="maintenance.daysOfMonth" type="checkbox" :value="lastDay.value" class="form-check-input">
<label class="form-check-label" :for="lastDay.langKey">
{{ $t(lastDay.langKey) }}
</label>
</div>
</div>
</template>
<!-- For any recurring types -->
<template v-if="maintenance.strategy === 'recurring-interval' || maintenance.strategy === 'recurring-weekday' || maintenance.strategy === 'recurring-day-of-month'">
<!-- Maintenance Time Window of a Day -->
<div class="my-3">
<label class="form-label">{{ $t("Maintenance Time Window of a Day") }}</label>
<Datepicker
v-model="maintenance.timeRange"
:dark="$root.isDark"
timePicker
disableTimeRangeValidation range
/>
</div>
<!-- Date Range -->
<div class="my-3">
<label class="form-label">{{ $t("Effective Date Range") }}</label>
<Datepicker
v-model="maintenance.dateRange"
:dark="$root.isDark"
range datePicker
:monthChangeOnScroll="false"
:minDate="minDate"
format="yyyy-MM-dd HH:mm:ss"
modelType="yyyy-MM-dd HH:mm:ss"
required
/>
</div>
</template>
<div class="mt-4 mb-1">
<button
id="monitor-submit-btn" class="btn btn-primary" type="submit"
:disabled="processing"
>
{{ $t("Save") }}
</button>
</div>
</div>
</div>
</div>
</form>
</div>
</transition>
</template>
<script>
import { useToast } from "vue-toastification";
import VueMultiselect from "vue-multiselect";
import dayjs from "dayjs";
import Datepicker from "@vuepic/vue-datepicker";
const toast = useToast();
export default {
components: {
VueMultiselect,
Datepicker
},
data() {
return {
processing: false,
maintenance: {},
affectedMonitors: [],
affectedMonitorsOptions: [],
showOnAllPages: false,
selectedStatusPages: [],
dark: (this.$root.theme === "dark"),
neverEnd: false,
minDate: this.$root.date(dayjs()) + " 00:00",
lastDays: [
{
langKey: "lastDay1",
value: "lastDay1",
},
{
langKey: "lastDay2",
value: "lastDay2",
},
{
langKey: "lastDay3",
value: "lastDay3",
},
{
langKey: "lastDay4",
value: "lastDay4",
}
],
weekdays: [
{
id: "weekday1",
langKey: "weekdayShortMon",
value: 1,
},
{
id: "weekday2",
langKey: "weekdayShortTue",
value: 2,
},
{
id: "weekday3",
langKey: "weekdayShortWed",
value: 3,
},
{
id: "weekday4",
langKey: "weekdayShortThu",
value: 4,
},
{
id: "weekday5",
langKey: "weekdayShortFri",
value: 5,
},
{
id: "weekday6",
langKey: "weekdayShortSat",
value: 6,
},
{
id: "weekday0",
langKey: "weekdayShortSun",
value: 0,
},
],
};
},
computed: {
selectedStatusPagesOptions() {
return Object.values(this.$root.statusPageList).map(statusPage => {
return {
id: statusPage.id,
name: statusPage.title
};
});
},
pageName() {
return this.$t((this.isAdd) ? "Schedule Maintenance" : "Edit Maintenance");
},
isAdd() {
return this.$route.path === "/add-maintenance";
},
isEdit() {
return this.$route.path.startsWith("/maintenance/edit");
},
},
watch: {
"$route.fullPath"() {
this.init();
},
neverEnd(value) {
if (value) {
this.maintenance.recurringEndDate = "";
}
},
},
mounted() {
this.init();
this.$root.getMonitorList((res) => {
if (res.ok) {
Object.values(this.$root.monitorList).map(monitor => {
this.affectedMonitorsOptions.push({
id: monitor.id,
name: monitor.name,
});
});
}
});
},
methods: {
init() {
this.affectedMonitors = [];
this.selectedStatusPages = [];
if (this.isAdd) {
this.maintenance = {
title: "",
description: "",
strategy: "single",
active: 1,
intervalDay: 1,
dateRange: [ this.minDate ],
timeRange: [{
hours: 2,
minutes: 0,
}, {
hours: 3,
minutes: 0,
}],
weekdays: [],
daysOfMonth: [],
};
} else if (this.isEdit) {
this.$root.getSocket().emit("getMaintenance", this.$route.params.id, (res) => {
if (res.ok) {
this.maintenance = res.maintenance;
this.$root.getSocket().emit("getMonitorMaintenance", this.$route.params.id, (res) => {
if (res.ok) {
Object.values(res.monitors).map(monitor => {
this.affectedMonitors.push(monitor);
});
} else {
toast.error(res.msg);
}
});
this.$root.getSocket().emit("getMaintenanceStatusPage", this.$route.params.id, (res) => {
if (res.ok) {
Object.values(res.statusPages).map(statusPage => {
this.selectedStatusPages.push({
id: statusPage.id,
name: statusPage.title
});
});
this.showOnAllPages = Object.values(res.statusPages).length === this.selectedStatusPagesOptions.length;
} else {
toast.error(res.msg);
}
});
} else {
toast.error(res.msg);
}
});
}
},
async submit() {
this.processing = true;
if (this.affectedMonitors.length === 0) {
toast.error(this.$t("atLeastOneMonitor"));
return this.processing = false;
}
if (this.isAdd) {
this.$root.addMaintenance(this.maintenance, async (res) => {
if (res.ok) {
await this.addMonitorMaintenance(res.maintenanceID, async () => {
await this.addMaintenanceStatusPage(res.maintenanceID, () => {
toast.success(res.msg);
this.processing = false;
this.$root.getMaintenanceList();
this.$router.push("/maintenance");
});
});
} else {
toast.error(res.msg);
this.processing = false;
}
});
} else {
this.$root.getSocket().emit("editMaintenance", this.maintenance, async (res) => {
if (res.ok) {
await this.addMonitorMaintenance(res.maintenanceID, async () => {
await this.addMaintenanceStatusPage(res.maintenanceID, () => {
this.processing = false;
this.$root.toastRes(res);
this.init();
this.$router.push("/maintenance");
});
});
} else {
this.processing = false;
toast.error(res.msg);
}
});
}
},
async addMonitorMaintenance(maintenanceID, callback) {
await this.$root.addMonitorMaintenance(maintenanceID, this.affectedMonitors, async (res) => {
if (!res.ok) {
toast.error(res.msg);
} else {
this.$root.getMonitorList();
}
callback();
});
},
async addMaintenanceStatusPage(maintenanceID, callback) {
await this.$root.addMaintenanceStatusPage(maintenanceID, (this.showOnAllPages) ? this.selectedStatusPagesOptions : this.selectedStatusPages, async (res) => {
if (!res.ok) {
toast.error(res.msg);
} else {
this.$root.getMaintenanceList();
}
callback();
});
},
},
};
</script>
<style lang="scss" scoped>
.shadow-box {
padding: 20px;
}
textarea {
min-height: 150px;
}
.dark-calendar::-webkit-calendar-picker-indicator {
filter: invert(1);
}
.weekday-picker {
display: flex;
gap: 10px;
& > div {
display: flex;
flex-direction: column;
align-items: center;
width: 40px;
.form-check-inline {
margin-right: 0;
}
}
}
.day-picker {
display: flex;
gap: 10px;
flex-wrap: wrap;
& > div {
display: flex;
flex-direction: column;
align-items: center;
width: 40px;
.form-check-inline {
margin-right: 0;
}
}
}
</style>

View file

@ -97,8 +97,8 @@
</div> </div>
<!-- Port --> <!-- Port -->
<!-- For TCP Port / Steam / MQTT Type --> <!-- For TCP Port / Steam / MQTT / Radius Type -->
<div v-if="monitor.type === 'port' || monitor.type === 'steam' || monitor.type === 'mqtt'" class="my-3"> <div v-if="monitor.type === 'port' || monitor.type === 'steam' || monitor.type === 'mqtt' || monitor.type === 'radius'" class="my-3">
<label for="port" class="form-label">{{ $t("Port") }}</label> <label for="port" class="form-label">{{ $t("Port") }}</label>
<input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1"> <input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1">
</div> </div>
@ -616,9 +616,11 @@ export default {
} }
// Set default port for DNS if not already defined // Set default port for DNS if not already defined
if (! this.monitor.port || this.monitor.port === "53") { if (! this.monitor.port || this.monitor.port === "53" || this.monitor.port === "1812") {
if (this.monitor.type === "dns") { if (this.monitor.type === "dns") {
this.monitor.port = "53"; this.monitor.port = "53";
} else if (this.monitor.type === "radius") {
this.monitor.port = "1812";
} else { } else {
this.monitor.port = undefined; this.monitor.port = undefined;
} }

Some files were not shown because too many files have changed in this diff Show more