Merge branch 'master' into master

This commit is contained in:
Moritz R 2022-06-15 11:33:00 +02:00 committed by GitHub
commit ac449ec1c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 4998 additions and 842 deletions

View File

@ -1,3 +1,5 @@
👉 Delete this line if you have read and agree our pull request rules and guidelines: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md#can-i-create-a-pull-request-for-uptime-kuma
# Description # Description
Fixes #(issue) Fixes #(issue)

View File

@ -27,17 +27,30 @@ 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?
(Updated 2022-04-24) Since I don't want to waste your time, be sure to create empty draft pull request, so we can discuss first. Yes, you can. However, since I don't want to waste your time, be sure to **create empty draft pull request, so we can discuss first** if it is a large pull request or you don't know it will be merged or not.
Also, please don't rush or ask for ETA, because I have to understand the pull request, make sure it is no breaking changes and stick to my vision of this project, especially for large 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.
✅ Accept: ✅ Accept:
- Bug/Security fix - Bug/Security fix
- Translations - Translations
- Adding notification providers - Adding notification providers
⚠️ Discuss First ⚠️ Discussion First
- Large pull requests - Large pull requests
- New features - New features
❌ Won't Merge
- Do not pass auto test
- Any breaking changes
- Duplicated pull request
- Buggy
- Existing logic is completely modified or deleted for no reason
- A function that is completely out of scope
### Recommended Pull Request Guideline ### Recommended Pull Request Guideline
Before deep into coding, discussion first is preferred. Creating an empty pull request for discussion would be recommended. Before deep into coding, discussion first is preferred. Creating an empty pull request for discussion would be recommended.
@ -53,22 +66,15 @@ Before deep into coding, discussion first is preferred. Creating an empty pull r
1. Click "Change to draft" 1. Click "Change to draft"
1. Discussion 1. Discussion
#### ❌ Won't Merge
- Any breaking changes
- Duplicated pull request
- Buggy
- Existing logic is completely modified or deleted
- A function that is completely out of scope
## Project Styles ## Project Styles
I personally do not like something need to learn so much and need to config so much before you can finally start the app. I personally do not like something need to learn so much and need to config so much before you can finally start the app.
- Easy to install for non-Docker users, no native build dependency is needed (at least for x86_64), no extra config, no extra effort to get it run - Easy to install for non-Docker users, no native build dependency is needed (at least for x86_64), no extra config, no extra effort to get it run
- Single container for Docker users, no very complex docker-compose file. Just map the volume and expose the port, then good to go - Single container for Docker users, no very complex docker-compose file. Just map the volume and expose the port, then good to go
- Settings should be configurable in the frontend. Env var is not encouraged. - Settings should be configurable in the frontend. Environment variable is not encouraged, unless it is related to startup such as `DATA_DIR`.
- Easy to use - Easy to use
- The web UI styling should be consistent and nice.
## Coding Styles ## Coding Styles

View File

@ -8,9 +8,6 @@ Do not use the issue tracker or discuss it in the public as it will cause more d
## Supported Versions ## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
### Uptime Kuma Versions ### Uptime Kuma Versions
You should use or upgrade to the latest version of Uptime Kuma. All `1.X.X` versions are upgradable to the lastest version. You should use or upgrade to the latest version of Uptime Kuma. All `1.X.X` versions are upgradable to the lastest version.

View File

@ -1,10 +1,14 @@
import legacy from "@vitejs/plugin-legacy"; import legacy from "@vitejs/plugin-legacy";
import vue from "@vitejs/plugin-vue"; import vue from "@vitejs/plugin-vue";
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import visualizer from "rollup-plugin-visualizer";
import viteCompression from "vite-plugin-compression";
const postCssScss = require("postcss-scss"); const postCssScss = require("postcss-scss");
const postcssRTLCSS = require("postcss-rtlcss"); const postcssRTLCSS = require("postcss-rtlcss");
const viteCompressionFilter = /\.(js|mjs|json|css|html|svg)$/i;
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
@ -12,7 +16,18 @@ export default defineConfig({
legacy({ legacy({
targets: [ "ie > 11" ], targets: [ "ie > 11" ],
additionalLegacyPolyfills: [ "regenerator-runtime/runtime" ] additionalLegacyPolyfills: [ "regenerator-runtime/runtime" ]
}) }),
visualizer({
filename: "tmp/dist-stats.html"
}),
viteCompression({
algorithm: "gzip",
filter: viteCompressionFilter,
}),
viteCompression({
algorithm: "brotliCompress",
filter: viteCompressionFilter,
}),
], ],
css: { css: {
postcss: { postcss: {
@ -21,4 +36,13 @@ export default defineConfig({
"plugins": [ postcssRTLCSS ] "plugins": [ postcssRTLCSS ]
} }
}, },
build: {
rollupOptions: {
output: {
manualChunks(id, { getModuleInfo, getModuleIds }) {
}
}
},
}
}); });

View File

@ -0,0 +1,18 @@
BEGIN TRANSACTION;
ALTER TABLE monitor
ADD auth_method VARCHAR(250);
ALTER TABLE monitor
ADD auth_domain TEXT;
ALTER TABLE monitor
ADD auth_workstation TEXT;
COMMIT;
BEGIN TRANSACTION;
UPDATE monitor
SET auth_method = 'basic'
WHERE basic_auth_user is not null;
COMMIT;

View File

@ -0,0 +1,10 @@
BEGIN TRANSACTION;
ALTER TABLE monitor
ADD database_connection_string VARCHAR(2000);
ALTER TABLE monitor
ADD database_query TEXT;
COMMIT

View File

@ -12,7 +12,8 @@ RUN apt update && \
apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \ apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
sqlite3 iputils-ping util-linux dumb-init && \ sqlite3 iputils-ping util-linux dumb-init && \
pip3 --no-cache-dir install apprise==0.9.8.3 && \ pip3 --no-cache-dir install apprise==0.9.8.3 && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/* && \
apt --yes autoremove
# Install cloudflared # Install cloudflared
# dpkg --add-architecture arm: cloudflared do not provide armhf, this is workaround. Read more: https://github.com/cloudflare/cloudflared/issues/583 # dpkg --add-architecture arm: cloudflared do not provide armhf, this is workaround. Read more: https://github.com/cloudflare/cloudflared/issues/583
@ -22,5 +23,6 @@ RUN node ./extra/download-cloudflared.js $TARGETPLATFORM && \
apt update && \ apt update && \
apt --yes --no-install-recommends install ./cloudflared.deb && \ apt --yes --no-install-recommends install ./cloudflared.deb && \
rm -rf /var/lib/apt/lists/* && \ rm -rf /var/lib/apt/lists/* && \
rm -f cloudflared.deb rm -f cloudflared.deb && \
apt --yes autoremove

4300
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.16.0", "version": "1.17.0-beta.0",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@ -39,7 +39,7 @@
"build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push", "build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
"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",
"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.16.0 && npm ci --production && npm run download-dist", "setup": "git checkout 1.16.1 && 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",
@ -57,7 +57,8 @@
"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",
"git-remove-tag": "git tag -d" "git-remove-tag": "git tag -d",
"build-dist-and-restart": "npm run build && npm run start-server-dev"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "~1.2.36", "@fortawesome/fontawesome-svg-core": "~1.2.36",
@ -68,6 +69,8 @@
"@popperjs/core": "~2.10.2", "@popperjs/core": "~2.10.2",
"args-parser": "~1.3.0", "args-parser": "~1.3.0",
"axios": "~0.26.1", "axios": "~0.26.1",
"axios-cached-dns-resolve": "^3.0.6",
"axios-ntlm": "^1.3.0",
"badge-maker": "^3.3.1", "badge-maker": "^3.3.1",
"bcryptjs": "~2.4.3", "bcryptjs": "~2.4.3",
"bootstrap": "5.1.3", "bootstrap": "5.1.3",
@ -76,12 +79,16 @@
"chart.js": "~3.6.2", "chart.js": "~3.6.2",
"chartjs-adapter-dayjs": "~1.0.0", "chartjs-adapter-dayjs": "~1.0.0",
"check-password-strength": "^2.0.5", "check-password-strength": "^2.0.5",
"cheerio": "^1.0.0-rc.10",
"chroma-js": "^2.1.2", "chroma-js": "^2.1.2",
"command-exists": "~1.2.9", "command-exists": "~1.2.9",
"compare-versions": "~3.6.0", "compare-versions": "~3.6.0",
"dayjs": "~1.10.8", "compression": "^1.7.4",
"dayjs": "^1.11.0",
"esm-wallaby": "^3.2.26",
"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",
"favico.js": "^0.3.10", "favico.js": "^0.3.10",
"form-data": "~4.0.0", "form-data": "~4.0.0",
"http-graceful-shutdown": "~3.1.7", "http-graceful-shutdown": "~3.1.7",
@ -92,6 +99,7 @@
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"limiter": "^2.1.0", "limiter": "^2.1.0",
"mqtt": "^4.2.8", "mqtt": "^4.2.8",
"mssql": "^8.1.0",
"node-cloudflared-tunnel": "~1.0.9", "node-cloudflared-tunnel": "~1.0.9",
"nodemailer": "~6.6.5", "nodemailer": "~6.6.5",
"notp": "~2.0.3", "notp": "~2.0.3",
@ -102,7 +110,7 @@
"prom-client": "~13.2.0", "prom-client": "~13.2.0",
"prometheus-api-metrics": "~3.2.1", "prometheus-api-metrics": "~3.2.1",
"qrcode": "~1.5.0", "qrcode": "~1.5.0",
"redbean-node": "0.1.3", "redbean-node": "0.1.4",
"socket.io": "~4.4.1", "socket.io": "~4.4.1",
"socket.io-client": "~4.4.1", "socket.io-client": "~4.4.1",
"socks-proxy-agent": "^6.1.1", "socks-proxy-agent": "^6.1.1",
@ -129,27 +137,31 @@
"@babel/eslint-parser": "~7.17.0", "@babel/eslint-parser": "~7.17.0",
"@babel/preset-env": "^7.15.8", "@babel/preset-env": "^7.15.8",
"@types/bootstrap": "~5.1.9", "@types/bootstrap": "~5.1.9",
"@vitejs/plugin-legacy": "~1.6.4", "@vitejs/plugin-legacy": "~1.8.2",
"@vitejs/plugin-vue": "~1.9.4", "@vitejs/plugin-vue": "~2.3.3",
"@vue/compiler-sfc": "~3.2.31", "@vue/compiler-sfc": "~3.2.36",
"aedes": "^0.46.3", "aedes": "^0.46.3",
"babel-plugin-rewire": "~1.2.0", "babel-plugin-rewire": "~1.2.0",
"concurrently": "^7.1.0", "concurrently": "^7.1.0",
"core-js": "~3.18.3", "core-js": "~3.18.3",
"cross-env": "~7.0.3", "cross-env": "~7.0.3",
"delay": "^5.0.0",
"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",
"jest": "~27.2.5", "jest": "~27.2.5",
"jest-puppeteer": "~6.0.3", "jest-puppeteer": "~6.0.3",
"lru-cache": "^7.7.1",
"npm-check-updates": "^12.5.9", "npm-check-updates": "^12.5.9",
"postcss-html": "^1.3.1", "postcss-html": "^1.3.1",
"puppeteer": "~13.1.3", "puppeteer": "~13.1.3",
"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",
"typescript": "~4.4.4", "typescript": "~4.4.4",
"vite": "~2.6.14", "vite": "~2.9.9",
"vite-plugin-compression": "^0.5.1",
"wait-on": "^6.0.1" "wait-on": "^6.0.1"
} }
} }

View File

@ -59,6 +59,8 @@ class Database {
"patch-monitor-expiry-notification.sql": true, "patch-monitor-expiry-notification.sql": true,
"patch-status-page-footer-css.sql": true, "patch-status-page-footer-css.sql": true,
"patch-added-mqtt-monitor.sql": true, "patch-added-mqtt-monitor.sql": true,
"patch-add-sqlserver-monitor.sql": true,
"patch-add-other-auth.sql": { parents: [ "patch-monitor-basic-auth.sql" ] },
}; };
/** /**

View File

@ -7,7 +7,7 @@ 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, flipStatus, TimeLogger } = require("../../src/util");
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mqttAsync } = require("../util-server"); const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, mqttAsync, setSetting, httpNtlm } = 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");
const { Notification } = require("../notification"); const { Notification } = require("../notification");
@ -17,6 +17,12 @@ 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 axiosCachedDnsResolve = require("esm-wallaby")(module)("axios-cached-dns-resolve");
// create an axios client instance with the cached DNS resolve interceptor
const axiosClient = axios.create();
axiosCachedDnsResolve.registerInterceptor(axiosClient);
/** /**
* status: * status:
* 0 = DOWN * 0 = DOWN
@ -91,7 +97,12 @@ class Monitor extends BeanModel {
mqttUsername: this.mqttUsername, mqttUsername: this.mqttUsername,
mqttPassword: this.mqttPassword, mqttPassword: this.mqttPassword,
mqttTopic: this.mqttTopic, mqttTopic: this.mqttTopic,
mqttSuccessMessage: this.mqttSuccessMessage mqttSuccessMessage: this.mqttSuccessMessage,
databaseConnectionString: this.databaseConnectionString,
databaseQuery: this.databaseQuery,
authMethod: this.authMethod,
authWorkstation: this.authWorkstation,
authDomain: this.authDomain,
}; };
if (includeSensitiveData) { if (includeSensitiveData) {
@ -196,7 +207,7 @@ class Monitor extends BeanModel {
let bean = R.dispense("heartbeat"); let bean = R.dispense("heartbeat");
bean.monitor_id = this.id; bean.monitor_id = this.id;
bean.time = R.isoDateTime(dayjs.utc()); bean.time = R.isoDateTimeMillis(dayjs.utc());
bean.status = DOWN; bean.status = DOWN;
if (this.isUpsideDown()) { if (this.isUpsideDown()) {
@ -217,7 +228,7 @@ class Monitor extends BeanModel {
// HTTP basic auth // HTTP basic auth
let basicAuthHeader = {}; let basicAuthHeader = {};
if (this.basic_auth_user) { if (this.auth_method === "basic") {
basicAuthHeader = { basicAuthHeader = {
"Authorization": "Basic " + this.encodeBase64(this.basic_auth_user, this.basic_auth_pass), "Authorization": "Basic " + this.encodeBase64(this.basic_auth_user, this.basic_auth_pass),
}; };
@ -268,7 +279,21 @@ class Monitor extends BeanModel {
log.debug("monitor", `[${this.name}] Axios Options: ${JSON.stringify(options)}`); log.debug("monitor", `[${this.name}] Axios Options: ${JSON.stringify(options)}`);
log.debug("monitor", `[${this.name}] Axios Request`); log.debug("monitor", `[${this.name}] Axios Request`);
let res = await axios.request(options); let res;
if (this.auth_method === "ntlm") {
options.httpsAgent.keepAlive = true;
res = await httpNtlm(options, {
username: this.basic_auth_user,
password: this.basic_auth_pass,
domain: this.authDomain,
workstation: this.authWorkstation ? this.authWorkstation : undefined
});
} else {
res = await axiosClient.request(options);
}
bean.msg = `${res.status} - ${res.statusText}`; bean.msg = `${res.status} - ${res.statusText}`;
bean.ping = dayjs().valueOf() - startTime; bean.ping = dayjs().valueOf() - startTime;
@ -316,7 +341,11 @@ class Monitor extends BeanModel {
bean.msg += ", keyword is found"; bean.msg += ", keyword is found";
bean.status = UP; bean.status = UP;
} else { } else {
throw new Error(bean.msg + ", but keyword is not found"); data = data.replace(/<[^>]*>?|[\n\r]|\s+/gm, " ");
if (data.length > 50) {
data = data.substring(0, 47) + "...";
}
throw new Error(bean.msg + ", but keyword is not in [" + data + "]");
} }
} }
@ -371,22 +400,33 @@ class Monitor extends BeanModel {
bean.msg = dnsMessage; bean.msg = dnsMessage;
bean.status = UP; bean.status = UP;
} else if (this.type === "push") { // Type: Push } else if (this.type === "push") { // Type: Push
const time = R.isoDateTime(dayjs.utc().subtract(this.interval, "second")); log.debug("monitor", `[${this.name}] Checking monitor at ${dayjs().format("YYYY-MM-DD HH:mm:ss.SSS")}`);
const bufferTime = 1000; // 1s buffer to accommodate clock differences
let heartbeatCount = await R.count("heartbeat", " monitor_id = ? AND time > ? ", [ if (previousBeat) {
this.id, const msSinceLastBeat = dayjs.utc().valueOf() - dayjs.utc(previousBeat.time).valueOf();
time
]);
log.debug("monitor", "heartbeatCount" + heartbeatCount + " " + time); log.debug("monitor", `[${this.name}] msSinceLastBeat = ${msSinceLastBeat}`);
if (heartbeatCount <= 0) { // If the previous beat was down or pending we use the regular
throw new Error("No heartbeat in the time window"); // beatInterval/retryInterval in the setTimeout further below
if (previousBeat.status !== (this.isUpsideDown() ? DOWN : UP) || msSinceLastBeat > beatInterval * 1000 + bufferTime) {
throw new Error("No heartbeat in the time window");
} else {
let timeout = beatInterval * 1000 - msSinceLastBeat;
if (timeout < 0) {
timeout = bufferTime;
} else {
timeout += bufferTime;
}
// No need to insert successful heartbeat for push type, so end here
retries = 0;
log.debug("monitor", `[${this.name}] timeout = ${timeout}`);
this.heartbeatInterval = setTimeout(beat, timeout);
return;
}
} else { } else {
// No need to insert successful heartbeat for push type, so end here throw new Error("No heartbeat in the time window");
retries = 0;
this.heartbeatInterval = setTimeout(beat, beatInterval * 1000);
return;
} }
} else if (this.type === "steam") { } else if (this.type === "steam") {
@ -398,7 +438,7 @@ class Monitor extends BeanModel {
throw new Error("Steam API Key not found"); throw new Error("Steam API Key not found");
} }
let res = await axios.get(steamApiUrl, { let res = await axiosClient.get(steamApiUrl, {
timeout: this.interval * 1000 * 0.8, timeout: this.interval * 1000 * 0.8,
headers: { headers: {
"Accept": "*/*", "Accept": "*/*",
@ -463,6 +503,14 @@ class Monitor extends BeanModel {
interval: this.interval, interval: this.interval,
}); });
bean.status = UP; bean.status = UP;
} else if (this.type === "sqlserver") {
let startTime = dayjs().valueOf();
await mssqlQuery(this.databaseConnectionString, this.databaseQuery);
bean.msg = "";
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else { } else {
bean.msg = "Unknown Monitor Type"; bean.msg = "Unknown Monitor Type";
bean.status = PENDING; bean.status = PENDING;
@ -513,7 +561,7 @@ class Monitor extends BeanModel {
} }
if (bean.status === UP) { if (bean.status === UP) {
log.info("monitor", `Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`); log.debug("monitor", `Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`);
} else if (bean.status === PENDING) { } else if (bean.status === PENDING) {
if (this.retryInterval > 0) { if (this.retryInterval > 0) {
beatInterval = this.retryInterval; beatInterval = this.retryInterval;
@ -857,10 +905,19 @@ class Monitor extends BeanModel {
if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) { if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) {
const notificationList = await Monitor.getNotificationList(this); const notificationList = await Monitor.getNotificationList(this);
log.debug("monitor", "call sendCertNotificationByTargetDays"); let notifyDays = await setting("tlsExpiryNotifyDays");
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 21, notificationList); if (notifyDays == null || !Array.isArray(notifyDays)) {
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 14, notificationList); // Reset Default
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 7, notificationList); setSetting("tlsExpiryNotifyDays", [ 7, 14, 21 ], "general");
notifyDays = [ 7, 14, 21 ];
}
if (notifyDays != null && Array.isArray(notifyDays)) {
for (const day of notifyDays) {
log.debug("monitor", "call sendCertNotificationByTargetDays", day);
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, day, notificationList);
}
}
} }
} }

View File

@ -1,10 +1,104 @@
const { BeanModel } = require("redbean-node/dist/bean-model"); const { BeanModel } = require("redbean-node/dist/bean-model");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const cheerio = require("cheerio");
const { UptimeKumaServer } = require("../uptime-kuma-server");
class StatusPage extends BeanModel { class StatusPage extends BeanModel {
/**
* Like this: { "test-uptime.kuma.pet": "default" }
* @type {{}}
*/
static domainMappingList = { }; static domainMappingList = { };
/**
*
* @param {Response} response
* @param {string} indexHTML
* @param {string} slug
*/
static async handleStatusPageResponse(response, indexHTML, slug) {
let statusPage = await R.findOne("status_page", " slug = ? ", [
slug
]);
if (statusPage) {
response.send(await StatusPage.renderHTML(indexHTML, statusPage));
} else {
response.status(404).send(UptimeKumaServer.getInstance().indexHTML);
}
}
/**
* SSR for status pages
* @param {string} indexHTML
* @param {StatusPage} statusPage
*/
static async renderHTML(indexHTML, statusPage) {
const $ = cheerio.load(indexHTML);
const description155 = statusPage.description?.substring(0, 155);
$("title").text(statusPage.title);
$("meta[name=description]").attr("content", description155);
if (statusPage.icon) {
$("link[rel=icon]")
.attr("href", statusPage.icon)
.removeAttr("type");
}
const head = $("head");
// OG Meta Tags
head.append(`<meta property="og:title" content="${statusPage.title}" />`);
head.append(`<meta property="og:description" content="${description155}" />`);
// Preload data
const json = JSON.stringify(await StatusPage.getStatusPageData(statusPage));
head.append(`
<script>
window.preloadData = ${json}
</script>
`);
return $.root().html();
}
/**
* Get all status page data in one call
* @param {StatusPage} statusPage
*/
static async getStatusPageData(statusPage) {
// Incident
let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [
statusPage.id,
]);
if (incident) {
incident = incident.toPublicJSON();
}
// Public Group List
const publicGroupList = [];
const showTags = !!statusPage.show_tags;
const list = await R.find("group", " public = 1 AND status_page_id = ? ORDER BY weight ", [
statusPage.id
]);
for (let groupBean of list) {
let monitorGroup = await groupBean.toPublicJSON(showTags);
publicGroupList.push(monitorGroup);
}
// Response
return {
config: await statusPage.toPublicJSON(),
incident,
publicGroupList
};
}
/** /**
* Loads domain mapping from DB * Loads domain mapping from DB
* Return object like this: { "test-uptime.kuma.pet": "default" } * Return object like this: { "test-uptime.kuma.pet": "default" }

View File

@ -0,0 +1,26 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Ntfy extends NotificationProvider {
name = "ntfy";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
await axios.post(`${notification.ntfyserverurl}`, {
"topic": notification.ntfytopic,
"message": msg,
"priority": notification.ntfyPriority || 4,
"title": "Uptime-Kuma",
});
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Ntfy;

View File

@ -2,6 +2,7 @@ const { R } = require("redbean-node");
const Apprise = require("./notification-providers/apprise"); const Apprise = require("./notification-providers/apprise");
const Discord = require("./notification-providers/discord"); const Discord = require("./notification-providers/discord");
const Gotify = require("./notification-providers/gotify"); const Gotify = require("./notification-providers/gotify");
const Ntfy = require("./notification-providers/ntfy");
const Line = require("./notification-providers/line"); const Line = require("./notification-providers/line");
const LunaSea = require("./notification-providers/lunasea"); const LunaSea = require("./notification-providers/lunasea");
const Mattermost = require("./notification-providers/mattermost"); const Mattermost = require("./notification-providers/mattermost");
@ -52,6 +53,7 @@ class Notification {
new Discord(), new Discord(),
new Teams(), new Teams(),
new Gotify(), new Gotify(),
new Ntfy(),
new Line(), new Line(),
new LunaSea(), new LunaSea(),
new Feishu(), new Feishu(),

View File

@ -1,5 +1,5 @@
let express = require("express"); let express = require("express");
const { allowDevAllOrigin, allowAllOrigin, percentageToColor, filterAndJoin } = require("../util-server"); const { allowDevAllOrigin, allowAllOrigin, percentageToColor, filterAndJoin, send403 } = require("../util-server");
const { R } = require("redbean-node"); 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");
@ -59,7 +59,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
let duration = 0; let duration = 0;
let bean = R.dispense("heartbeat"); let bean = R.dispense("heartbeat");
bean.time = R.isoDateTime(dayjs.utc()); bean.time = R.isoDateTimeMillis(dayjs.utc());
if (previousHeartbeat) { if (previousHeartbeat) {
isFirstBeat = false; isFirstBeat = false;
@ -67,6 +67,7 @@ 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");
} }
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);
@ -91,115 +92,13 @@ router.get("/api/push/:pushToken", async (request, response) => {
} }
} catch (e) { } catch (e) {
response.json({ response.status(404).json({
ok: false, ok: false,
msg: e.message msg: e.message
}); });
} }
}); });
// Status page config, incident, monitor list
router.get("/api/status-page/:slug", cache("5 minutes"), async (request, response) => {
allowDevAllOrigin(response);
let slug = request.params.slug;
// Get Status Page
let statusPage = await R.findOne("status_page", " slug = ? ", [
slug
]);
if (!statusPage) {
response.statusCode = 404;
response.json({
msg: "Not Found"
});
return;
}
try {
// Incident
let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [
statusPage.id,
]);
if (incident) {
incident = incident.toPublicJSON();
}
// Public Group List
const publicGroupList = [];
const showTags = !!statusPage.show_tags;
const list = await R.find("group", " public = 1 AND status_page_id = ? ORDER BY weight ", [
statusPage.id
]);
for (let groupBean of list) {
let monitorGroup = await groupBean.toPublicJSON(showTags);
publicGroupList.push(monitorGroup);
}
// Response
response.json({
config: await statusPage.toPublicJSON(),
incident,
publicGroupList
});
} catch (error) {
send403(response, error.message);
}
});
// Status Page Polling Data
// Can fetch only if published
router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (request, response) => {
allowDevAllOrigin(response);
try {
let heartbeatList = {};
let uptimeList = {};
let slug = request.params.slug;
let statusPageID = await StatusPage.slugToID(slug);
let monitorIDList = await R.getCol(`
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
WHERE monitor_group.group_id = \`group\`.id
AND public = 1
AND \`group\`.status_page_id = ?
`, [
statusPageID
]);
for (let monitorID of monitorIDList) {
let list = await R.getAll(`
SELECT * FROM heartbeat
WHERE monitor_id = ?
ORDER BY time DESC
LIMIT 50
`, [
monitorID,
]);
list = R.convertToBeans("heartbeat", list);
heartbeatList[monitorID] = list.reverse().map(row => row.toPublicJSON());
const type = 24;
uptimeList[`${monitorID}_${type}`] = await Monitor.calcUptime(type, monitorID);
}
response.json({
heartbeatList,
uptimeList
});
} catch (error) {
send403(response, error.message);
}
});
router.get("/api/badge/:id/status", cache("5 minutes"), async (request, response) => { router.get("/api/badge/:id/status", cache("5 minutes"), async (request, response) => {
allowAllOrigin(response); allowAllOrigin(response);
@ -376,16 +275,4 @@ router.get("/api/badge/:id/ping/:duration?", cache("5 minutes"), async (request,
} }
}); });
/**
* Send a 403 response
* @param {Object} res Express response object
* @param {string} [msg=""] Message to send
*/
function send403(res, msg = "") {
res.status(403).json({
"status": "fail",
"msg": msg,
});
}
module.exports = router; module.exports = router;

View File

@ -0,0 +1,110 @@
let express = require("express");
const apicache = require("../modules/apicache");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const StatusPage = require("../model/status_page");
const { allowDevAllOrigin, send403 } = require("../util-server");
const { R } = require("redbean-node");
const Monitor = require("../model/monitor");
let router = express.Router();
let cache = apicache.middleware;
const server = UptimeKumaServer.getInstance();
router.get("/status/:slug", cache("5 minutes"), async (request, response) => {
let slug = request.params.slug;
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
});
router.get("/status", cache("5 minutes"), async (request, response) => {
let slug = "default";
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
});
router.get("/status-page", cache("5 minutes"), async (request, response) => {
let slug = "default";
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
});
// Status page config, incident, monitor list
router.get("/api/status-page/:slug", cache("5 minutes"), async (request, response) => {
allowDevAllOrigin(response);
let slug = request.params.slug;
try {
// Get Status Page
let statusPage = await R.findOne("status_page", " slug = ? ", [
slug
]);
if (!statusPage) {
return null;
}
let statusPageData = await StatusPage.getStatusPageData(statusPage);
if (!statusPageData) {
response.statusCode = 404;
response.json({
msg: "Not Found"
});
return;
}
// Response
response.json(statusPageData);
} catch (error) {
send403(response, error.message);
}
});
// Status Page Polling Data
// Can fetch only if published
router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (request, response) => {
allowDevAllOrigin(response);
try {
let heartbeatList = {};
let uptimeList = {};
let slug = request.params.slug;
let statusPageID = await StatusPage.slugToID(slug);
let monitorIDList = await R.getCol(`
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
WHERE monitor_group.group_id = \`group\`.id
AND public = 1
AND \`group\`.status_page_id = ?
`, [
statusPageID
]);
for (let monitorID of monitorIDList) {
let list = await R.getAll(`
SELECT * FROM heartbeat
WHERE monitor_id = ?
ORDER BY time DESC
LIMIT 50
`, [
monitorID,
]);
list = R.convertToBeans("heartbeat", list);
heartbeatList[monitorID] = list.reverse().map(row => row.toPublicJSON());
const type = 24;
uptimeList[`${monitorID}_${type}`] = await Monitor.calcUptime(type, monitorID);
}
response.json({
heartbeatList,
uptimeList
});
} catch (error) {
send403(response, error.message);
}
});
module.exports = router;

View File

@ -16,7 +16,7 @@ if (nodeVersion < requiredVersion) {
} }
const args = require("args-parser")(process.argv); const args = require("args-parser")(process.argv);
const { sleep, log, getRandomInt, genSecret, debug, isDev } = require("../src/util"); const { sleep, log, getRandomInt, genSecret, isDev } = require("../src/util");
const config = require("./config"); const config = require("./config");
log.info("server", "Welcome to Uptime Kuma"); log.info("server", "Welcome to Uptime Kuma");
@ -35,6 +35,7 @@ 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");
log.debug("server", "Importing redbean-node"); log.debug("server", "Importing redbean-node");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
log.debug("server", "Importing jsonwebtoken"); log.debug("server", "Importing jsonwebtoken");
@ -148,22 +149,6 @@ let jwtSecret = null;
*/ */
let needSetup = false; let needSetup = false;
/**
* Cache Index HTML
* @type {string}
*/
let indexHTML = "";
try {
indexHTML = fs.readFileSync("./dist/index.html").toString();
} catch (e) {
// "dist/index.html" is not necessary for development
if (process.env.NODE_ENV !== "development") {
log.error("server", "Error: Cannot find 'dist/index.html', did you install correctly?");
process.exit(1);
}
}
(async () => { (async () => {
Database.init(args); Database.init(args);
await initDatabase(testMode); await initDatabase(testMode);
@ -179,13 +164,17 @@ try {
// Entry Page // Entry Page
app.get("/", async (request, response) => { app.get("/", async (request, response) => {
debug(`Request Domain: ${request.hostname}`); log.debug("entry", `Request Domain: ${request.hostname}`);
if (request.hostname in StatusPage.domainMappingList) { if (request.hostname in StatusPage.domainMappingList) {
debug("This is a status page domain"); log.debug("entry", "This is a status page domain");
response.send(indexHTML);
let slug = StatusPage.domainMappingList[request.hostname];
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
} else if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) { } else if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) {
response.redirect("/status/" + exports.entryPage.replace("statusPage-", "")); response.redirect("/status/" + exports.entryPage.replace("statusPage-", ""));
} else { } else {
response.redirect("/dashboard"); response.redirect("/dashboard");
} }
@ -214,7 +203,9 @@ try {
// With Basic Auth using the first user's username/password // With Basic Auth using the first user's username/password
app.get("/metrics", basicAuth, prometheusAPIMetrics()); app.get("/metrics", basicAuth, prometheusAPIMetrics());
app.use("/", express.static("dist")); app.use("/", expressStaticGzip("dist", {
enableBrotli: true,
}));
// ./data/upload // ./data/upload
app.use("/upload", express.static(Database.uploadDir)); app.use("/upload", express.static(Database.uploadDir));
@ -227,12 +218,16 @@ try {
const apiRouter = require("./routers/api-router"); const apiRouter = require("./routers/api-router");
app.use(apiRouter); app.use(apiRouter);
// Status Page Router
const statusPageRouter = require("./routers/status-page-router");
app.use(statusPageRouter);
// Universal Route Handler, must be at the end of all express routes. // Universal Route Handler, must be at the end of all express routes.
app.get("*", async (_request, response) => { app.get("*", async (_request, response) => {
if (_request.originalUrl.startsWith("/upload/")) { if (_request.originalUrl.startsWith("/upload/")) {
response.status(404).send("File not found."); response.status(404).send("File not found.");
} else { } else {
response.send(indexHTML); response.send(server.indexHTML);
} }
}); });
@ -677,6 +672,11 @@ try {
bean.mqttPassword = monitor.mqttPassword; bean.mqttPassword = monitor.mqttPassword;
bean.mqttTopic = monitor.mqttTopic; bean.mqttTopic = monitor.mqttTopic;
bean.mqttSuccessMessage = monitor.mqttSuccessMessage; bean.mqttSuccessMessage = monitor.mqttSuccessMessage;
bean.databaseConnectionString = monitor.databaseConnectionString;
bean.databaseQuery = monitor.databaseQuery;
bean.authMethod = monitor.authMethod;
bean.authWorkstation = monitor.authWorkstation;
bean.authDomain = monitor.authDomain;
await R.store(bean); await R.store(bean);
@ -1250,8 +1250,11 @@ try {
method: monitorListData[i].method || "GET", method: monitorListData[i].method || "GET",
body: monitorListData[i].body, body: monitorListData[i].body,
headers: monitorListData[i].headers, headers: monitorListData[i].headers,
authMethod: monitorListData[i].authMethod,
basic_auth_user: monitorListData[i].basic_auth_user, basic_auth_user: monitorListData[i].basic_auth_user,
basic_auth_pass: monitorListData[i].basic_auth_pass, basic_auth_pass: monitorListData[i].basic_auth_pass,
authWorkstation: monitorListData[i].authWorkstation,
authDomain: monitorListData[i].authDomain,
interval: monitorListData[i].interval, interval: monitorListData[i].interval,
retryInterval: retryInterval, retryInterval: retryInterval,
hostname: monitorListData[i].hostname, hostname: monitorListData[i].hostname,

View File

@ -29,6 +29,12 @@ class UptimeKumaServer {
httpServer = undefined; httpServer = undefined;
io = undefined; io = undefined;
/**
* Cache Index HTML
* @type {string}
*/
indexHTML = "";
static getInstance(args) { static getInstance(args) {
if (UptimeKumaServer.instance == null) { if (UptimeKumaServer.instance == null) {
UptimeKumaServer.instance = new UptimeKumaServer(args); UptimeKumaServer.instance = new UptimeKumaServer(args);
@ -55,6 +61,16 @@ class UptimeKumaServer {
this.httpServer = http.createServer(this.app); this.httpServer = http.createServer(this.app);
} }
try {
this.indexHTML = fs.readFileSync("./dist/index.html").toString();
} catch (e) {
// "dist/index.html" is not necessary for development
if (process.env.NODE_ENV !== "development") {
log.error("server", "Error: Cannot find 'dist/index.html', did you install correctly?");
process.exit(1);
}
}
this.io = new Server(this.httpServer); this.io = new Server(this.httpServer);
} }

View File

@ -10,6 +10,8 @@ const chardet = require("chardet");
const mqtt = require("mqtt"); const mqtt = require("mqtt");
const chroma = require("chroma-js"); const chroma = require("chroma-js");
const { badgeConstants } = require("./config"); const { badgeConstants } = require("./config");
const mssql = require("mssql");
const { NtlmClient } = require("axios-ntlm");
// From ping-lite // From ping-lite
exports.WIN = /^win/.test(process.platform); exports.WIN = /^win/.test(process.platform);
@ -172,6 +174,26 @@ exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
}); });
}; };
/**
* Use NTLM Auth for a http request.
* @param {Object} options The http request options
* @param {Object} ntlmOptions The auth options
* @returns {Promise<(string[]|Object[]|Object)>}
*/
exports.httpNtlm = function (options, ntlmOptions) {
return new Promise((resolve, reject) => {
let client = NtlmClient(ntlmOptions);
client(options)
.then((resp) => {
resolve(resp);
})
.catch((err) => {
reject(err);
});
});
};
/** /**
* Resolves a given record using the specified DNS server * Resolves a given record using the specified DNS server
* @param {string} hostname The hostname of the record to lookup * @param {string} hostname The hostname of the record to lookup
@ -185,7 +207,7 @@ exports.dnsResolve = function (hostname, resolverServer, resolverPort, rrtype) {
// Remove brackets from IPv6 addresses so we can re-add them to // Remove brackets from IPv6 addresses so we can re-add them to
// prevent issues with ::1:5300 (::1 port 5300) // prevent issues with ::1:5300 (::1 port 5300)
resolverServer = resolverServer.replace("[", "").replace("]", ""); resolverServer = resolverServer.replace("[", "").replace("]", "");
resolver.setServers([`[${resolverServer}]:${resolverPort}`]); resolver.setServers([ `[${resolverServer}]:${resolverPort}` ]);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (rrtype === "PTR") { if (rrtype === "PTR") {
resolver.reverse(hostname, (err, records) => { resolver.reverse(hostname, (err, records) => {
@ -207,6 +229,31 @@ exports.dnsResolve = function (hostname, resolverServer, resolverPort, rrtype) {
}); });
}; };
/**
* Run a query on SQL Server
* @param {string} connectionString The database connection string
* @param {string} query The query to validate the database with
* @returns {Promise<(string[]|Object[]|Object)>}
*/
exports.mssqlQuery = function (connectionString, query) {
return new Promise((resolve, reject) => {
mssql.on("error", err => {
reject(err);
});
mssql.connect(connectionString).then(pool => {
return pool.request()
.query(query);
}).then(result => {
resolve(result);
}).catch(err => {
reject(err);
}).finally(() => {
mssql.close();
});
});
};
/** /**
* Retrieve value of setting based on key * Retrieve value of setting based on key
* @param {string} key Key of setting to retrieve * @param {string} key Key of setting to retrieve
@ -558,3 +605,15 @@ exports.percentageToColor = (percentage, maxHue = 90, minHue = 10) => {
exports.filterAndJoin = (parts, connector = "") => { exports.filterAndJoin = (parts, connector = "") => {
return parts.filter((part) => !!part && part !== "").join(connector); return parts.filter((part) => !!part && part !== "").join(connector);
}; };
/**
* Send a 403 response
* @param {Object} res Express response object
* @param {string} [msg=""] Message to send
*/
module.exports.send403 = (res, msg = "") => {
res.status(403).json({
"status": "fail",
"msg": msg,
});
};

View File

@ -34,6 +34,25 @@ textarea.form-control {
} }
} }
// optgroup
optgroup {
color: #b1b1b1;
option {
color: #212529;
}
}
.dark {
optgroup {
color: #535864;
option {
color: $dark-font-color;
}
}
}
// Scrollbar
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: #ccc; background: #ccc;
border-radius: 20px; border-radius: 20px;
@ -363,6 +382,12 @@ textarea.form-control {
overflow-y: auto; overflow-y: auto;
height: calc(100% - 65px); height: calc(100% - 65px);
} }
@media (max-width: 770px) {
&.scrollbar {
height: calc(100% - 40px);
}
}
.item { .item {
display: block; display: block;
@ -473,6 +498,14 @@ textarea.form-control {
outline: none !important; outline: none !important;
} }
h5.settings-subheading::after {
content: "";
display: block;
width: 50%;
padding-top: 8px;
border-bottom: 1px solid $dark-border-color;
}
// Localization // Localization
@import "localization.scss"; @import "localization.scss";

View File

@ -0,0 +1,86 @@
<template>
<div class="input-group mb-3">
<input
ref="input"
v-model="model"
class="form-control"
:type="type"
:placeholder="placeholder"
:disabled="!enabled"
>
<a class="btn btn-outline-primary" @click="action()">
<font-awesome-icon :icon="icon" />
</a>
</div>
</template>
<script>
/**
* Generic input field with a customizable action on the right.
* Action is passed in as a function.
*/
export default {
props: {
/**
* The value of the input field.
*/
modelValue: {
type: String,
default: ""
},
/**
* Whether the input field is enabled / disabled.
*/
enabled: {
type: Boolean,
default: true
},
/**
* Placeholder text for the input field.
*/
placeholder: {
type: String,
default: ""
},
/**
* The icon displayed in the right button of the input field.
* Accepts a Font Awesome icon string identifier.
* @example "plus"
*/
icon: {
type: String,
required: true,
},
/**
* The input type of the input field.
* @example "email"
*/
type: {
type: String,
default: "text",
},
/**
* The action to be performed when the button is clicked.
* Action is passed in as a function.
*/
action: {
type: Function,
default: () => {},
}
},
emits: [ "update:modelValue" ],
computed: {
/**
* Send value update to parent on change.
*/
model: {
get() {
return this.modelValue;
},
set(value) {
this.$emit("update:modelValue", value);
}
}
},
};
</script>

View File

@ -69,10 +69,22 @@ export default {
}; };
}, },
computed: { computed: {
/**
* Improve the sticky appearance of the list by increasing its
* height as user scrolls down.
* Not used on mobile.
*/
boxStyle() { boxStyle() {
return { if (window.innerWidth > 550) {
height: `calc(100vh - 160px + ${this.windowTop}px)`, return {
}; height: `calc(100vh - 160px + ${this.windowTop}px)`,
};
} else {
return {
height: "calc(100vh - 160px)",
};
}
}, },
sortedMonitorList() { sortedMonitorList() {

View File

@ -41,7 +41,7 @@
<Uptime :monitor="monitor.element" type="24" :pill="true" /> <Uptime :monitor="monitor.element" type="24" :pill="true" />
{{ monitor.element.name }} {{ monitor.element.name }}
</div> </div>
<div v-if="showTag" class="tags"> <div v-if="showTags" class="tags">
<Tag v-for="tag in monitor.element.tags" :key="tag" :item="tag" :size="'sm'" /> <Tag v-for="tag in monitor.element.tags" :key="tag" :item="tag" :size="'sm'" />
</div> </div>
</div> </div>

View File

@ -0,0 +1,30 @@
<template>
<div class="mb-3">
<label for="ntfy-ntfytopic" class="form-label">{{ $t("ntfy Topic") }}</label>
<div class="input-group mb-3">
<input id="ntfy-ntfytopic" v-model="$parent.notification.ntfytopic" type="text" class="form-control" required>
</div>
</div>
<div class="mb-3">
<label for="ntfy-server-url" class="form-label">{{ $t("Server URL") }}</label>
<div class="input-group mb-3">
<input id="ntfy-server-url" v-model="$parent.notification.ntfyserverurl" type="text" class="form-control" required>
</div>
</div>
<div class="mb-3">
<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">
</div>
</template>
<script>
export default {
mounted() {
if (typeof this.$parent.notification.ntfyPriority === "undefined") {
this.$parent.notification.ntfyserverurl = "https://ntfy.sh";
this.$parent.notification.ntfyPriority = 5;
}
},
};
</script>

View File

@ -1,8 +1,8 @@
<template> <template>
<div class="mb-3"> <div class="mb-3">
<label for="promosms-login" class="form-label">{{ $("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">{{ $("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="one-time-code"></HiddenInput>
</div> </div>
<div class="mb-3"> <div class="mb-3">

View File

@ -4,6 +4,7 @@ import Discord from "./Discord.vue";
import Webhook from "./Webhook.vue"; import Webhook from "./Webhook.vue";
import Signal from "./Signal.vue"; import Signal from "./Signal.vue";
import Gotify from "./Gotify.vue"; import Gotify from "./Gotify.vue";
import Ntfy from "./Ntfy.vue";
import Slack from "./Slack.vue"; import Slack from "./Slack.vue";
import RocketChat from "./RocketChat.vue"; import RocketChat from "./RocketChat.vue";
import Teams from "./Teams.vue"; import Teams from "./Teams.vue";
@ -46,6 +47,7 @@ const NotificationFormList = {
"teams": Teams, "teams": Teams,
"signal": Signal, "signal": Signal,
"gotify": Gotify, "gotify": Gotify,
"ntfy": Ntfy,
"slack": Slack, "slack": Slack,
"rocket.chat": RocketChat, "rocket.chat": RocketChat,
"pushover": Pushover, "pushover": Pushover,

View File

@ -20,16 +20,91 @@
</button> </button>
</div> </div>
<div class="my-4 pt-4">
<h5 class="my-4 settings-subheading">{{ $t("settingsCertificateExpiry") }}</h5>
<p>{{ $t("certificationExpiryDescription") }}</p>
<div class="mt-1 mb-3 ps-2 cert-exp-days col-12 col-xl-6">
<div v-for="day in settings.tlsExpiryNotifyDays" :key="day" class="d-flex align-items-center justify-content-between cert-exp-day-row py-2">
<span>{{ day }} {{ $tc("day", day) }}</span>
<button type="button" class="btn-rm-expiry btn btn-outline-danger ms-2 py-1" @click="removeExpiryNotifDay(day)">
<font-awesome-icon class="" icon="times" />
</button>
</div>
</div>
<div class="col-12 col-xl-6">
<ActionInput v-model="expiryNotifInput" :type="'number'" :placeholder="$t('day')" :icon="'plus'" :action="() => addExpiryNotifDay(expiryNotifInput)" />
</div>
<div>
<button class="btn btn-primary" type="button" @click="saveSettings()">
{{ $t("Save") }}
</button>
</div>
</div>
<NotificationDialog ref="notificationDialog" /> <NotificationDialog ref="notificationDialog" />
</div> </div>
</template> </template>
<script> <script>
import NotificationDialog from "../../components/NotificationDialog.vue"; import NotificationDialog from "../../components/NotificationDialog.vue";
import ActionInput from "../ActionInput.vue";
export default { export default {
components: { components: {
NotificationDialog NotificationDialog,
ActionInput,
},
data() {
return {
/**
* Variable to store the input for new certificate expiry day.
*/
expiryNotifInput: null,
};
},
computed: {
settings() {
return this.$parent.$parent.$parent.settings;
},
saveSettings() {
return this.$parent.$parent.$parent.saveSettings;
},
settingsLoaded() {
return this.$parent.$parent.$parent.settingsLoaded;
},
},
methods: {
/**
* Remove a day from expiry notification days.
* @param {number} day The day to remove.
*/
removeExpiryNotifDay(day) {
this.settings.tlsExpiryNotifyDays = this.settings.tlsExpiryNotifyDays.filter(d => d !== day);
},
/**
* Add a new expiry notification day.
* Will verify:
* - day is not null or empty string.
* - day is a number.
* - day is > 0.
* - The day is not already in the list.
* @param {number} day The day number to add.
*/
addExpiryNotifDay(day) {
if (day != null && day !== "") {
const parsedDay = parseInt(day);
if (parsedDay != null && !isNaN(parsedDay) && parsedDay > 0) {
if (!this.settings.tlsExpiryNotifyDays.includes(parsedDay)) {
this.settings.tlsExpiryNotifyDays.push(parseInt(day));
this.settings.tlsExpiryNotifyDays.sort((a, b) => a - b);
this.expiryNotifInput = null;
}
}
}
},
}, },
}; };
</script> </script>
@ -37,10 +112,27 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../assets/vars.scss"; @import "../../assets/vars.scss";
.btn-rm-expiry {
padding-left: 11px;
padding-right: 11px;
}
.dark { .dark {
.list-group-item { .list-group-item {
background-color: $dark-bg2; background-color: $dark-bg2;
color: $dark-font-color; color: $dark-font-color;
} }
} }
.cert-exp-days .cert-exp-day-row {
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
.dark & {
border-bottom: 1px solid $dark-border-color;
}
}
.cert-exp-days .cert-exp-day-row:last-child {
border: none;
}
</style> </style>

View File

@ -8,7 +8,7 @@
<button v-if="! settings.disableAuth" id="logout-btn" class="btn btn-danger ms-4 me-2 mb-2" @click="$root.logout">{{ $t("Logout") }}</button> <button v-if="! settings.disableAuth" id="logout-btn" class="btn btn-danger ms-4 me-2 mb-2" @click="$root.logout">{{ $t("Logout") }}</button>
</p> </p>
<h5 class="my-4">{{ $t("Change Password") }}</h5> <h5 class="my-4 settings-subheading">{{ $t("Change Password") }}</h5>
<form class="mb-3" @submit.prevent="savePassword"> <form class="mb-3" @submit.prevent="savePassword">
<div class="mb-3"> <div class="mb-3">
<label for="current-password" class="form-label"> <label for="current-password" class="form-label">
@ -62,7 +62,7 @@
</template> </template>
<div v-if="! settings.disableAuth" class="mt-5 mb-3"> <div v-if="! settings.disableAuth" class="mt-5 mb-3">
<h5 class="my-4"> <h5 class="my-4 settings-subheading">
{{ $t("Two Factor Authentication") }} {{ $t("Two Factor Authentication") }}
</h5> </h5>
<div class="mb-4"> <div class="mb-4">
@ -78,7 +78,7 @@
<div class="my-4"> <div class="my-4">
<!-- Advanced --> <!-- Advanced -->
<h5 class="my-4">{{ $t("Advanced") }}</h5> <h5 class="my-4 settings-subheading">{{ $t("Advanced") }}</h5>
<div class="mb-4"> <div class="mb-4">
<button v-if="settings.disableAuth" id="enableAuth-btn" class="btn btn-outline-primary me-2 mb-2" @click="enableAuth">{{ $t("Enable Auth") }}</button> <button v-if="settings.disableAuth" id="enableAuth-btn" class="btn btn-outline-primary me-2 mb-2" @click="enableAuth">{{ $t("Enable Auth") }}</button>
@ -346,15 +346,3 @@ export default {
}, },
}; };
</script> </script>
<style lang="scss" scoped>
@import "../../assets/vars.scss";
h5::after {
content: "";
display: block;
width: 50%;
padding-top: 8px;
border-bottom: 1px solid $dark-border-color;
}
</style>

View File

@ -55,8 +55,7 @@ export default {
Current: "Текущ", Current: "Текущ",
Uptime: "Достъпност", Uptime: "Достъпност",
"Cert Exp.": "Вал. сертификат", "Cert Exp.": "Вал. сертификат",
days: "дни", day: "ден | дни",
day: "ден",
"-day": "-дни", "-day": "-дни",
hour: "час", hour: "час",
"-hour": "-часa", "-hour": "-часa",
@ -515,4 +514,18 @@ export default {
"Go back to the previous page.": "Да се върнете към предишната страница.", "Go back to the previous page.": "Да се върнете към предишната страница.",
"Coming Soon": "Очаквайте скоро", "Coming Soon": "Очаквайте скоро",
wayToGetClickSendSMSToken: "Може да получите API потребителско име и API ключ от {0} .", wayToGetClickSendSMSToken: "Може да получите API потребителско име и API ключ от {0} .",
dnsPortDescription: "DNS порт на сървъра. По подразбиране е 53, но може да бъде променен по всяко време.",
error: "грешка",
critical: "критична",
wayToGetPagerDutyKey: "Може да го получите като посетите Service -> Service Directory -> (Select a service) -> Integrations -> Add integration. Тук може да потърсите \"Events API V2\". Повече информация {0}",
"Integration Key": "Ключ за интегриране",
"Integration URL": "URL адрес за интеграция",
"Auto resolve or acknowledged": "Автоматично разрешаване или потвърждаване",
"do nothing": "не прави нищо",
"auto acknowledged": "автоматично потвърждаване",
"auto resolve": "автоматично потвърждаване",
"Connection String": "Стринг за връзка",
Query: "Заявка",
settingsCertificateExpiry: "Изтичане валидността на TLS сертификата",
certificationExpiryDescription: "HTTPS мониторите задействат известие при изтичане на TLS сертификата в:",
}; };

View File

@ -56,8 +56,7 @@ export default {
Current: "Aktuální", Current: "Aktuální",
Uptime: "Doba provozu", Uptime: "Doba provozu",
"Cert Exp.": "Platnost certifikátu", "Cert Exp.": "Platnost certifikátu",
days: "dny/í", day: "den | dny/í",
day: "den",
"-day": "-dní", "-day": "-dní",
hour: "hodina", hour: "hodina",
"-hour": "-hodin", "-hour": "-hodin",

View File

@ -30,8 +30,7 @@ export default {
Current: "Aktuelt", Current: "Aktuelt",
Uptime: "Oppetid", Uptime: "Oppetid",
"Cert Exp.": "Certifikatets udløb", "Cert Exp.": "Certifikatets udløb",
days: "Dage", day: "Dag | Dage",
day: "Dag",
"-day": "-Dage", "-day": "-Dage",
hour: "Timer", hour: "Timer",
"-hour": "-Timer", "-hour": "-Timer",

View File

@ -30,8 +30,7 @@ export default {
Current: "Aktuell", Current: "Aktuell",
Uptime: "Verfügbarkeit", Uptime: "Verfügbarkeit",
"Cert Exp.": "Zertifikatsablauf", "Cert Exp.": "Zertifikatsablauf",
days: "Tage", day: "Tag | Tage",
day: "Tag",
"-day": "-Tage", "-day": "-Tage",
hour: "Stunde", hour: "Stunde",
"-hour": "-Stunden", "-hour": "-Stunden",

View File

@ -57,8 +57,7 @@ export default {
Current: "Current", Current: "Current",
Uptime: "Uptime", Uptime: "Uptime",
"Cert Exp.": "Cert Exp.", "Cert Exp.": "Cert Exp.",
days: "days", day: "day | days",
day: "day",
"-day": "-day", "-day": "-day",
hour: "hour", hour: "hour",
"-hour": "-hour", "-hour": "-hour",
@ -531,4 +530,8 @@ export default {
"Go back to the previous page.": "Go back to the previous page.", "Go back to the previous page.": "Go back to the previous page.",
"Coming Soon": "Coming Soon", "Coming Soon": "Coming Soon",
wayToGetClickSendSMSToken: "You can get API Username and API Key from {0} .", wayToGetClickSendSMSToken: "You can get API Username and API Key from {0} .",
"Connection String": "Connection String",
"Query": "Query",
settingsCertificateExpiry: "TLS Certificate Expiry",
certificationExpiryDescription: "HTTPS Monitors trigger notification when TLS certificate expires in:",
}; };

View File

@ -44,8 +44,7 @@ export default {
Current: "Actual", Current: "Actual",
Uptime: "Tiempo activo", Uptime: "Tiempo activo",
"Cert Exp.": "Caducidad cert.", "Cert Exp.": "Caducidad cert.",
days: "días", day: "día | días",
day: "día",
"-day": "-día", "-day": "-día",
hour: "hora", hour: "hora",
"-hour": "-hora", "-hour": "-hora",

View File

@ -47,8 +47,7 @@ export default {
Current: "Hetkeseisund", Current: "Hetkeseisund",
Uptime: "Eluiga", Uptime: "Eluiga",
"Cert Exp.": "Sert. aegumine", "Cert Exp.": "Sert. aegumine",
days: "päeva", day: "päev | päeva",
day: "päev",
"-day": "-päev", "-day": "-päev",
hour: "tund", hour: "tund",
"-hour": "-tund", "-hour": "-tund",

View File

@ -55,7 +55,6 @@ export default {
Current: "فعلی", Current: "فعلی",
Uptime: "آپتایم", Uptime: "آپتایم",
"Cert Exp.": "تاریخ انقضای SSL", "Cert Exp.": "تاریخ انقضای SSL",
days: "روز",
day: "روز", day: "روز",
"-day": "-روز", "-day": "-روز",
hour: "ساعت", hour: "ساعت",

View File

@ -55,8 +55,7 @@ export default {
Current: "Actuellement", Current: "Actuellement",
Uptime: "Uptime", Uptime: "Uptime",
"Cert Exp.": "Expiration SSL", "Cert Exp.": "Expiration SSL",
days: "jours", day: "jour | jours",
day: "jour",
"-day": "-jours", "-day": "-jours",
hour: "-heure", hour: "-heure",
"-hour": "-heures", "-hour": "-heures",

View File

@ -56,8 +56,7 @@ export default {
Current: "Trenutno", Current: "Trenutno",
Uptime: "Dostupnost", Uptime: "Dostupnost",
"Cert Exp.": "Istek cert.", "Cert Exp.": "Istek cert.",
days: "dana", day: "dan | dana",
day: "dan",
"-day": "-dnevno", "-day": "-dnevno",
hour: "sat", hour: "sat",
"-hour": "-satno", "-hour": "-satno",

View File

@ -55,7 +55,6 @@ export default {
Current: "Aktuális", Current: "Aktuális",
Uptime: "Uptime", Uptime: "Uptime",
"Cert Exp.": "SSL lejárat", "Cert Exp.": "SSL lejárat",
days: "nap",
day: "nap", day: "nap",
"-day": " nap", "-day": " nap",
hour: "óra", hour: "óra",

View File

@ -55,8 +55,7 @@ export default {
Current: "Saat ini", Current: "Saat ini",
Uptime: "Waktu aktif", Uptime: "Waktu aktif",
"Cert Exp.": "Cert Exp.", "Cert Exp.": "Cert Exp.",
days: "hari-hari", day: "hari | hari-hari",
day: "hari",
"-day": "-hari", "-day": "-hari",
hour: "Jam", hour: "Jam",
"-hour": "-Jam", "-hour": "-Jam",

View File

@ -56,8 +56,7 @@ export default {
Current: "Corrente", Current: "Corrente",
Uptime: "Tempo di attività", Uptime: "Tempo di attività",
"Cert Exp.": "Scadenza certificato", "Cert Exp.": "Scadenza certificato",
days: "giorni", day: "giorno | giorni",
day: "giorno",
"-day": "-giorni", "-day": "-giorni",
hour: "ora", hour: "ora",
"-hour": "-ore", "-hour": "-ore",

View File

@ -44,8 +44,7 @@ export default {
Current: "現在", Current: "現在",
Uptime: "起動時間", Uptime: "起動時間",
"Cert Exp.": "証明書有効期限", "Cert Exp.": "証明書有効期限",
days: "日間", day: "日 | 日間",
day: "日",
"-day": "-日", "-day": "-日",
hour: "時間", hour: "時間",
"-hour": "-時間", "-hour": "-時間",

View File

@ -55,7 +55,6 @@ export default {
Current: "현재", Current: "현재",
Uptime: "업타임", Uptime: "업타임",
"Cert Exp.": "인증서 만료", "Cert Exp.": "인증서 만료",
days: "일",
day: "일", day: "일",
"-day": "-일", "-day": "-일",
hour: "시간", hour: "시간",
@ -187,9 +186,9 @@ export default {
"Bot Token": "봇 토큰", "Bot Token": "봇 토큰",
wayToGetTelegramToken: "토큰은 여기서 얻을 수 있어요: {0}.", wayToGetTelegramToken: "토큰은 여기서 얻을 수 있어요: {0}.",
"Chat ID": "채팅 ID", "Chat ID": "채팅 ID",
supportTelegramChatID: "Direct Chat / Group / Channel's Chat ID를 지원해요.", supportTelegramChatID: "개인 채팅 / 그룹 / 채널의 ID를 지원해요.",
wayToGetTelegramChatID: "봇에 메시지를 보내 채팅 ID를 얻고 밑에 URL로 이동해 chat_id를 볼 수 있어요.", wayToGetTelegramChatID: "봇에 메시지를 보내 채팅 ID를 얻고 밑에 URL로 이동해 chat_id를 볼 수 있어요.",
"YOUR BOT TOKEN HERE": "여기에 BOT 토큰을 적어주세요.", "YOUR BOT TOKEN HERE": "봇 토큰",
chatIDNotFound: "채팅 ID를 찾을 수 없어요. 먼저 봇에게 메시지를 보내주세요.", chatIDNotFound: "채팅 ID를 찾을 수 없어요. 먼저 봇에게 메시지를 보내주세요.",
webhook: "Webhook", webhook: "Webhook",
"Post URL": "Post URL", "Post URL": "Post URL",
@ -305,13 +304,13 @@ export default {
PasswordsDoNotMatch: "비밀번호가 일치하지 않아요.", PasswordsDoNotMatch: "비밀번호가 일치하지 않아요.",
records: "records", records: "records",
"One record": "One record", "One record": "One record",
steamApiKeyDescription: "스팀 게임 서버를 모니터링하려면 Steam Web API 키가 필요해요. API 키는 하단 사이트에서 등록할 수 있어요: ", steamApiKeyDescription: "스팀 게임 서버를 모니터링하려면 Steam Web API 키가 필요해요. API 키는 하단 사이트에서 등록할 수 있어요: ",
"Current User": "현재 사용자", "Current User": "현재 사용자",
recent: "최근", recent: "최근",
Done: "완료", Done: "완료",
Info: "정보", Info: "정보",
Security: "보안", Security: "보안",
"Steam API Key": "Steam API Key", "Steam API Key": "스팀 API 키",
"Shrink Database": "데이터베이스 축소", "Shrink Database": "데이터베이스 축소",
"Pick a RR-Type...": "RR-Type을 골라주세요...", "Pick a RR-Type...": "RR-Type을 골라주세요...",
"Pick Accepted Status Codes...": "상태 코드를 골라주세요...", "Pick Accepted Status Codes...": "상태 코드를 골라주세요...",
@ -352,4 +351,177 @@ export default {
serwersmsPhoneNumber: "휴대전화 번호", serwersmsPhoneNumber: "휴대전화 번호",
serwersmsSenderName: "보내는 사람 이름 (customer portal를 통해 가입된 정보)", serwersmsSenderName: "보내는 사람 이름 (customer portal를 통해 가입된 정보)",
stackfield: "Stackfield", stackfield: "Stackfield",
dnsPortDescription: "DNS 서버 포트, 기본값은 53 이에요. 포트는 언제나 변경할 수 있어요.",
PushByTechulus: "Push by Techulus",
GoogleChat: "Google Chat (Google Workspace only)",
topic: "Topic",
topicExplanation: "모니터링할 MQTT Topic",
successMessage: "성공 메시지",
successMessageExplanation: "성공으로 간주되는 MQTT 메시지",
error: "error",
critical: "critical",
Customize: "커스터마이즈",
"Custom Footer": "커스텀 Footer",
"Custom CSS": "커스텀 CSS",
smtpDkimSettings: "DKIM 설정",
smtpDkimDesc: "사용 방법은 DKIM {0}를 참조하세요.",
documentation: "문서",
smtpDkimDomain: "도메인 이름",
smtpDkimKeySelector: "Key Selector",
smtpDkimPrivateKey: "Private Key",
smtpDkimHashAlgo: "해시 알고리즘 (선택)",
smtpDkimheaderFieldNames: "서명할 헤더 키 (선택)",
smtpDkimskipFields: "서명하지 않을 헤더 키 (선택)",
wayToGetPagerDutyKey: "Service -> Service Directory -> (서비스 선택) -> Integrations -> Add integration. 에서 찾을 수 있어요. 자세히 알아보려면 {0}에서 \"Events API V2\"를 검색해봐요.",
"Integration Key": "Integration 키",
"Integration URL": "Integration URL",
"Auto resolve or acknowledged": "자동 해결 혹은 승인",
"do nothing": "아무것도 하지 않기",
"auto acknowledged": "자동 승인 (acknowledged)",
"auto resolve": "자동 해결 (resolve)",
gorush: "Gorush",
alerta: "Alerta",
alertaApiEndpoint: "API Endpoint",
alertaEnvironment: "환경변수",
alertaApiKey: "API 키",
alertaAlertState: "경고 상태",
alertaRecoverState: "해결된 상태",
deleteStatusPageMsg: "정말 이 상태 페이지를 삭제할까요?",
Proxies: "프록시",
default: "Default",
enabled: "활성화",
setAsDefault: "기본 프록시로 설정",
deleteProxyMsg: "정말 이 프록시를 모든 모니터링에서 삭제할까요?",
proxyDescription: "프록시가 작동하려면 모니터에 할당되어야 해요.",
enableProxyDescription: "이 프록시는 활성화될 때까지 영향을 미치지 않아요. 활성화 상태에 따라 모든 모니터에서 프록시를 일시정지할 수 있어요.",
setAsDefaultProxyDescription: "새로 추가하는 모든 모니터링에 이 프록시를 기본적으로 활성화해요. 각 모니터에 대해 별도로 프록시를 비활성화할 수 있어요.",
"Certificate Chain": "인증서 체인",
Valid: "유효",
Invalid: "유효하지 않음",
AccessKeyId: "AccessKey ID",
SecretAccessKey: "AccessKey Secret",
PhoneNumbers: "휴대전화 번호",
TemplateCode: "템플릿 코드",
SignName: "SignName",
"Sms template must contain parameters: ": "Sms 템플릿은 다음과 같은 파라미터가 포함되어야 해요:",
"Bark Endpoint": "Bark Endpoint",
WebHookUrl: "웹훅 URL",
SecretKey: "Secret Key",
"For safety, must use secret key": "안전을 위해 꼭 Secret Key를 사용하세요.",
"Device Token": "기기 Token",
Platform: "플랫폼",
iOS: "iOS",
Android: "Android",
Huawei: "Huawei",
High: "High",
Retry: "재시도",
Topic: "Topic",
"WeCom Bot Key": "WeCom Bot Key",
"Setup Proxy": "프록시 설정",
"Proxy Protocol": "프록시 프로토콜",
"Proxy Server": "프록시 서버",
"Proxy server has authentication": "프록시 서버에 인증 절차가 있음",
User: "사용자",
Installed: "설치됨",
"Not installed": "설치되어 있지 않음",
Running: "작동 중",
"Not running": "작동하고 있지 않음",
"Remove Token": "토큰 삭제",
Start: "시작",
Stop: "정지",
"Uptime Kuma": "Uptime Kuma",
"Add New Status Page": "새로운 상태 페이지 만들기",
Slug: "주소",
"Accept characters:": "허용되는 문자열:",
startOrEndWithOnly: "{0}로 시작하거나 끝나야 해요.",
"No consecutive dashes": "연속되는 대시는 허용되지 않아요",
Next: "다음",
"The slug is already taken. Please choose another slug.": "이미 존재하는 주소에요. 다른 주소를 사용해 주세요.",
"No Proxy": "프록시 없음",
"HTTP Basic Auth": "HTTP 인증",
"New Status Page": "새로운 상태 페이지",
"Page Not Found": "페이지를 찾을 수 없어요",
"Reverse Proxy": "리버스 프록시",
Backup: "백업",
About: "정보",
wayToGetCloudflaredURL: "({0}에서 Cloudflare 다운로드 하기)",
cloudflareWebsite: "Cloudflare 웹사이트",
"Message:": "메시지:",
"Don't know how to get the token? Please read the guide:": "토큰을 얻는 방법은 이 가이드를 확인해주세요:",
"The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "Cloudflare Tunnel를 연결하면 현재 연결이 끊길 수 있어요. 정말 중지할까요? 비밀번호를 입력해 확인하세요.",
"Other Software": "다른 소프트웨어",
"For example: nginx, Apache and Traefik.": "nginx, Apache, Traefik 등을 사용할 수 있어요.",
"Please read": "이 문서를 참조하세요:",
"Subject:": "Subject:",
"Valid To:": "Valid To:",
"Days Remaining:": "남은 일수:",
"Issuer:": "Issuer:",
"Fingerprint:": "Fingerprint:",
"No status pages": "상태 페이지 없음",
"Domain Name Expiry Notification": "도메인 이름 만료 알림",
Proxy: "프록시",
"Date Created": "생성된 날짜",
onebotHttpAddress: "OneBot HTTP 주소",
onebotMessageType: "OneBot 메시지 종류",
onebotGroupMessage: "그룹 메시지",
onebotPrivateMessage: "개인 메시지",
onebotUserOrGroupId: "그룹/사용자 ID",
onebotSafetyTips: "안전을 위해 Access 토큰을 설정하세요.",
"PushDeer Key": "PushDeer 키",
"Footer Text": "Footer 문구",
"Show Powered By": "Powered By 문구 표시하기",
"Domain Names": "도메인 이름",
signedInDisp: "{0} 로그인됨",
signedInDispDisabled: "인증 비활성화됨.",
"Certificate Expiry Notification": "인증서 만료 알림",
"API Username": "API 사용자 이름",
"API Key": "API 키",
"Recipient Number": "받는 사람 번호",
"From Name/Number": "발신자 이름/번호",
"Leave blank to use a shared sender number.": "공유 발신자 번호를 사용하려면 공백으로 두세요.",
"Octopush API Version": "Octopush API 버전",
"Legacy Octopush-DM": "레거시 Octopush-DM",
endpoint: "endpoint",
octopushAPIKey: "제어판 HTTP API credentials 에서 \"API key\"",
octopushLogin: "제어판 HTTP API credentials 에서 \"Login\"",
promosmsLogin: "API 로그인 이름",
promosmsPassword: "API 비밀번호",
"pushoversounds pushover": "Pushover (기본)",
"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": "진동만",
"pushoversounds none": "없음 (무음)",
pushyAPIKey: "비밀 API 키",
pushyToken: "기기 토큰",
"Show update if available": "사용 가능한 경우에 업데이트 표시",
"Also check beta release": "베타 릴리즈 확인",
"Using a Reverse Proxy?": "리버스 프록시를 사용하시나요?",
"Check how to config it for WebSocket": "웹소켓에 대한 설정 방법 확인",
"Steam Game Server": "스팀 게임 서버",
"Most likely causes:": "원인:",
"The resource is no longer available.": "더이상 사용할 수 없어요.",
"There might be a typing error in the address.": "주소에 오탈자가 있을 수 있어요.",
"What you can try:": "해결 방법:",
"Retype the address.": "주소 다시 입력하기",
"Go back to the previous page.": "이전 페이지로 돌아가기",
"Coming Soon": "Coming Soon",
wayToGetClickSendSMSToken: "{0}에서 API 사용자 이름과 키를 얻을 수 있어요.",
}; };

View File

@ -55,8 +55,7 @@ export default {
Current: "Nåværende", Current: "Nåværende",
Uptime: "Oppetid", Uptime: "Oppetid",
"Cert Exp.": "Sertifikat utløper", "Cert Exp.": "Sertifikat utløper",
days: "dager", day: "dag | dager",
day: "dag",
"-day": "-dag", "-day": "-dag",
hour: "time", hour: "time",
"-hour": "-time", "-hour": "-time",

View File

@ -52,8 +52,7 @@ export default {
Current: "Huidig", Current: "Huidig",
Uptime: "Uptime", Uptime: "Uptime",
"Cert Exp.": "Cert. verl.", "Cert Exp.": "Cert. verl.",
days: "dagen", day: "dag | dagen",
day: "dag",
"-day": "-dag", "-day": "-dag",
hour: "uur", hour: "uur",
"-hour": "-uur", "-hour": "-uur",

View File

@ -55,8 +55,7 @@ export default {
Current: "Aktualny", Current: "Aktualny",
Uptime: "Czas pracy", Uptime: "Czas pracy",
"Cert Exp.": "Certyfikat wygasa", "Cert Exp.": "Certyfikat wygasa",
days: "dni", day: "dzień | dni",
day: "dzień",
"-day": " dni", "-day": " dni",
hour: "godzina", hour: "godzina",
"-hour": " godzin", "-hour": " godzin",

View File

@ -55,8 +55,7 @@ export default {
Current: "Atual", Current: "Atual",
Uptime: "Tempo de atividade", Uptime: "Tempo de atividade",
"Cert Exp.": "Cert Exp.", "Cert Exp.": "Cert Exp.",
days: "dias", day: "dia | dias",
day: "dia",
"-day": "-dia", "-day": "-dia",
hour: "hora", hour: "hora",
"-hour": "-hora", "-hour": "-hora",

View File

@ -44,8 +44,7 @@ export default {
Current: "Текущий", Current: "Текущий",
Uptime: "Аптайм", Uptime: "Аптайм",
"Cert Exp.": "Сертификат истекает", "Cert Exp.": "Сертификат истекает",
days: "дней", day: "день | дней",
day: "день",
"-day": " дней", "-day": " дней",
hour: "час", hour: "час",
"-hour": " часа", "-hour": " часа",

View File

@ -56,8 +56,7 @@ export default {
Current: "Trenutno", Current: "Trenutno",
Uptime: "Uptime", Uptime: "Uptime",
"Cert Exp.": "Potek certifikata", "Cert Exp.": "Potek certifikata",
days: "dni", day: "dan | dni",
day: "dan",
"-day": "-dni", "-day": "-dni",
hour: "ura", hour: "ura",
"-hour": "-ur", "-hour": "-ur",

View File

@ -44,8 +44,7 @@ export default {
Current: "Trenutno", Current: "Trenutno",
Uptime: "Vreme rada", Uptime: "Vreme rada",
"Cert Exp.": "Istek sert.", "Cert Exp.": "Istek sert.",
days: "dana", day: "dan | dana",
day: "dan",
"-day": "-dana", "-day": "-dana",
hour: "sat", hour: "sat",
"-hour": "-sata", "-hour": "-sata",

View File

@ -44,8 +44,7 @@ export default {
Current: "Тренутно", Current: "Тренутно",
Uptime: "Време рада", Uptime: "Време рада",
"Cert Exp.": "Истек серт.", "Cert Exp.": "Истек серт.",
days: "дана", day: "дан | дана",
day: "дан",
"-day": "-дана", "-day": "-дана",
hour: "сат", hour: "сат",
"-hour": "-сата", "-hour": "-сата",

View File

@ -44,8 +44,7 @@ export default {
Current: "Nuvarande", Current: "Nuvarande",
Uptime: "Drifttid", Uptime: "Drifttid",
"Cert Exp.": "Certifikat utgår", "Cert Exp.": "Certifikat utgår",
days: "dagar", day: "dag | dagar",
day: "dag",
"-day": " dagar", "-day": " dagar",
hour: "timme", hour: "timme",
"-hour": " timmar", "-hour": " timmar",

View File

@ -57,8 +57,7 @@ export default {
Current: "Şu anda", Current: "Şu anda",
Uptime: "Çalışma zamanı", Uptime: "Çalışma zamanı",
"Cert Exp.": "Sertifika Süresi", "Cert Exp.": "Sertifika Süresi",
days: "günler", day: "gün | günler",
day: "gün",
"-day": "-gün", "-day": "-gün",
hour: "saat", hour: "saat",
"-hour": "-saat", "-hour": "-saat",
@ -516,4 +515,13 @@ export default {
"Go back to the previous page.": "Bir önceki sayfaya geri git.", "Go back to the previous page.": "Bir önceki sayfaya geri git.",
"Coming Soon": "Yakında gelecek", "Coming Soon": "Yakında gelecek",
wayToGetClickSendSMSToken: "API Kullanıcı Adı ve API Anahtarını {0} adresinden alabilirsiniz.", wayToGetClickSendSMSToken: "API Kullanıcı Adı ve API Anahtarını {0} adresinden alabilirsiniz.",
error: "hata",
critical: "kritik",
wayToGetPagerDutyKey: "Bunu şuraya giderek alabilirsiniz: Servis -> Servis Dizini -> (Bir servis seçin) -> Entegrasyonlar -> Entegrasyon ekle. Burada \"Events API V2\" için arama yapabilirsiniz. Daha fazla bilgi {0}",
"Integration Key": "Entegrasyon Anahtarı",
"Integration URL": "Entegrasyon URL",
"Auto resolve or acknowledged": "Otomatik çözümleme veya onaylama",
"do nothing": "hiçbir şey yapma",
"auto acknowledged": "otomatik onaylama",
"auto resolve": "otomatik çözümleme",
}; };

View File

@ -44,8 +44,7 @@ export default {
Current: "Поточний", Current: "Поточний",
Uptime: "Аптайм", Uptime: "Аптайм",
"Cert Exp.": "Сертифікат спливає", "Cert Exp.": "Сертифікат спливає",
days: "днів", day: "день | днів",
day: "день",
"-day": " днів", "-day": " днів",
hour: "година", hour: "година",
"-hour": " години", "-hour": " години",

View File

@ -56,7 +56,6 @@ export default {
Current: "Hiện tại", Current: "Hiện tại",
Uptime: "Uptime", Uptime: "Uptime",
"Cert Exp.": "Cert hết hạn", "Cert Exp.": "Cert hết hạn",
days: "ngày",
day: "ngày", day: "ngày",
"-day": "-ngày", "-day": "-ngày",
hour: "giờ", hour: "giờ",

View File

@ -57,7 +57,6 @@ export default {
Current: "当前", Current: "当前",
Uptime: "在线时间", Uptime: "在线时间",
"Cert Exp.": "证书有效期", "Cert Exp.": "证书有效期",
days: "天",
day: "天", day: "天",
"-day": " 天", "-day": " 天",
hour: "小时", hour: "小时",
@ -520,4 +519,14 @@ export default {
wayToGetClickSendSMSToken: "您可以从 {0} 获取 API 凭证 Username 和 凭证 Key。", wayToGetClickSendSMSToken: "您可以从 {0} 获取 API 凭证 Username 和 凭证 Key。",
signedInDisp: "当前用户: {0}", signedInDisp: "当前用户: {0}",
signedInDispDisabled: "已禁用身份验证", signedInDispDisabled: "已禁用身份验证",
dnsPortDescription: "DNS 服务器端口,默认为 53你可以在任何时候更改此端口.",
error: "错误",
critical: "关键",
wayToGetPagerDutyKey: "你可以在 Service -> Service Directory -> (Select a service) -> Integrations -> Add integration 页面中搜索 \"Events API V2\" 以获取此 Integration Key更多信息请参见 {0}",
"Integration Key": "Integration Key",
"Integration URL": "Integration URL",
"Auto resolve or acknowledged": "自动标记为已解决或已读",
"do nothing": "不做任何操作",
"auto acknowledged": "自动标记为已读",
"auto resolve": "自动标记为已解决",
}; };

View File

@ -30,7 +30,6 @@ export default {
Current: "目前", Current: "目前",
Uptime: "上線率", Uptime: "上線率",
"Cert Exp.": "証書期限", "Cert Exp.": "証書期限",
days: "日",
day: "日", day: "日",
"-day": "日", "-day": "日",
hour: "小時", hour: "小時",

View File

@ -56,7 +56,6 @@ export default {
Current: "目前", Current: "目前",
Uptime: "運作率", Uptime: "運作率",
"Cert Exp.": "憑證期限", "Cert Exp.": "憑證期限",
days: "天",
day: "天", day: "天",
"-day": "天", "-day": "天",
hour: "小時", hour: "小時",

View File

@ -77,7 +77,7 @@
<h4>{{ $t("Cert Exp.") }}</h4> <h4>{{ $t("Cert Exp.") }}</h4>
<p>(<Datetime :value="tlsInfo.certInfo.validTo" date-only />)</p> <p>(<Datetime :value="tlsInfo.certInfo.validTo" date-only />)</p>
<span class="num"> <span class="num">
<a href="#" @click.prevent="toggleCertInfoBox = !toggleCertInfoBox">{{ tlsInfo.certInfo.daysRemaining }} {{ $t("days") }}</a> <a href="#" @click.prevent="toggleCertInfoBox = !toggleCertInfoBox">{{ tlsInfo.certInfo.daysRemaining }} {{ $tc("day", tlsInfo.certInfo.daysRemaining) }}</a>
</span> </span>
</div> </div>
</div> </div>

View File

@ -11,33 +11,44 @@
<div class="my-3"> <div class="my-3">
<label for="type" class="form-label">{{ $t("Monitor Type") }}</label> <label for="type" class="form-label">{{ $t("Monitor Type") }}</label>
<select id="type" v-model="monitor.type" class="form-select"> <select id="type" v-model="monitor.type" class="form-select">
<option value="http"> <optgroup label="General Monitor Type">
HTTP(s) <option value="http">
</option> HTTP(s)
<option value="port"> </option>
TCP Port <option value="port">
</option> TCP Port
<option value="ping"> </option>
Ping <option value="ping">
</option> Ping
<option value="keyword"> </option>
HTTP(s) - {{ $t("Keyword") }} <option value="keyword">
</option> HTTP(s) - {{ $t("Keyword") }}
<option value="dns"> </option>
DNS <option value="dns">
</option> DNS
<option value="push"> </option>
Push <option value="docker">
</option> {{ $t("Docker Container") }}
<option value="steam"> </option>
{{ $t("Steam Game Server") }} </optgroup>
</option>
<option value="mqtt"> <optgroup label="Passive Monitor Type">
MQTT <option value="push">
</option> Push
<option value="docker"> </option>
{{ $t("Docker Container") }} </optgroup>
</option>
<optgroup label="Specific Monitor Type">
<option value="steam">
{{ $t("Steam Game Server") }}
</option>
<option value="mqtt">
MQTT
</option>
<option value="sqlserver">
SQL Server
</option>
</optgroup>
</select> </select>
</div> </div>
@ -188,6 +199,18 @@
</div> </div>
</template> </template>
<!-- SQL Server -->
<template v-if="monitor.type === 'sqlserver'">
<div class="my-3">
<label for="sqlserverConnectionString" class="form-label">SQL Server {{ $t("Connection String") }}</label>
<input id="sqlserverConnectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control">
</div>
<div class="my-3">
<label for="sqlserverQuery" class="form-label">SQL Server {{ $t("Query") }}</label>
<textarea id="sqlserverQuery" v-model="monitor.databaseQuery" class="form-control" placeholder="Example: select getdate()"></textarea>
</div>
</template>
<!-- Interval --> <!-- Interval -->
<div class="my-3"> <div class="my-3">
<label for="interval" class="form-label">{{ $t("Heartbeat Interval") }} ({{ $t("checkEverySecond", [ monitor.interval ]) }})</label> <label for="interval" class="form-label">{{ $t("Heartbeat Interval") }} ({{ $t("checkEverySecond", [ monitor.interval ]) }})</label>
@ -376,18 +399,46 @@
<textarea id="headers" v-model="monitor.headers" class="form-control" :placeholder="headersPlaceholder"></textarea> <textarea id="headers" v-model="monitor.headers" class="form-control" :placeholder="headersPlaceholder"></textarea>
</div> </div>
<!-- HTTP Basic Auth --> <!-- HTTP Auth -->
<h4 class="mt-5 mb-2">{{ $t("HTTP Basic Auth") }}</h4> <h4 class="mt-5 mb-2">{{ $t("HTTP Authentication") }}</h4>
<!-- Method -->
<div class="my-3"> <div class="my-3">
<label for="basicauth" class="form-label">{{ $t("Username") }}</label> <label for="method" class="form-label">{{ $t("Method") }}</label>
<input id="basicauth-user" v-model="monitor.basic_auth_user" type="text" class="form-control" :placeholder="$t('Username')"> <select id="method" v-model="monitor.authMethod" class="form-select">
<option :value="null">
None
</option>
<option value="basic">
Basic
</option>
<option value="ntlm">
NTLM
</option>
</select>
</div> </div>
<template v-if="monitor.authMethod && monitor.authMethod !== null ">
<div class="my-3">
<label for="basicauth" class="form-label">{{ $t("Username") }}</label>
<input id="basicauth-user" v-model="monitor.basic_auth_user" type="text" class="form-control" :placeholder="$t('Username')">
</div>
<div class="my-3"> <div class="my-3">
<label for="basicauth" class="form-label">{{ $t("Password") }}</label> <label for="basicauth" class="form-label">{{ $t("Password") }}</label>
<input id="basicauth-pass" v-model="monitor.basic_auth_pass" type="password" autocomplete="new-password" class="form-control" :placeholder="$t('Password')"> <input id="basicauth-pass" v-model="monitor.basic_auth_pass" type="password" autocomplete="new-password" class="form-control" :placeholder="$t('Password')">
</div> </div>
<template v-if="monitor.authMethod === 'ntlm' ">
<div class="my-3">
<label for="basicauth" class="form-label">{{ $t("Domain") }}</label>
<input id="basicauth-domain" v-model="monitor.authDomain" type="text" class="form-control" :placeholder="$t('Domain')">
</div>
<div class="my-3">
<label for="basicauth" class="form-label">{{ $t("Workstation") }}</label>
<input id="basicauth-workstation" v-model="monitor.authWorkstation" type="text" class="form-control" :placeholder="$t('Workstation')">
</div>
</template>
</template>
</template> </template>
</div> </div>
</div> </div>
@ -563,6 +614,7 @@ export default {
method: "GET", method: "GET",
interval: 60, interval: 60,
retryInterval: this.interval, retryInterval: this.interval,
databaseConnectionString: "Server=<hostname>,<port>;Database=<your database>;User Id=<your user id>;Password=<your password>;Encrypt=<true/false>;TrustServerCertificate=<Yes/No>;Connection Timeout=<int>",
maxretries: 0, maxretries: 0,
notificationIDList: {}, notificationIDList: {},
ignoreTls: false, ignoreTls: false,
@ -580,6 +632,7 @@ export default {
mqttPassword: "", mqttPassword: "",
mqttTopic: "", mqttTopic: "",
mqttSuccessMessage: "", mqttSuccessMessage: "",
authMethod: null,
}; };
if (this.$root.proxyList && !this.monitor.proxyId) { if (this.$root.proxyList && !this.monitor.proxyId) {

View File

@ -1,6 +1,6 @@
<template> <template>
<transition name="slide-fade" appear> <transition name="slide-fade" appear>
<MonitorList /> <MonitorList :scrollbar="true" />
</transition> </transition>
</template> </template>
@ -14,3 +14,11 @@ export default {
}; };
</script> </script>
<style lang="scss" scoped>
@import "../assets/vars";
.shadow-box {
padding: 20px;
}
</style>

View File

@ -32,6 +32,7 @@
<ul> <ul>
<li>{{ $t("Retype the address.") }}</li> <li>{{ $t("Retype the address.") }}</li>
<li><a href="#" class="go-back" @click="goBack()">{{ $t("Go back to the previous page.") }}</a></li> <li><a href="#" class="go-back" @click="goBack()">{{ $t("Go back to the previous page.") }}</a></li>
<li><a href="/" class="go-back">Go back to home page.</a></li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -145,6 +145,10 @@ export default {
this.settings.keepDataPeriodDays = 180; this.settings.keepDataPeriodDays = 180;
} }
if (this.settings.tlsExpiryNotifyDays === undefined) {
this.settings.tlsExpiryNotifyDays = [ 7, 14, 21 ];
}
this.settingsLoaded = true; this.settingsLoaded = true;
}); });
}, },

View File

@ -98,7 +98,7 @@
<h1 class="mb-4 title-flex"> <h1 class="mb-4 title-flex">
<!-- Logo --> <!-- Logo -->
<span class="logo-wrapper" @click="showImageCropUploadMethod"> <span class="logo-wrapper" @click="showImageCropUploadMethod">
<img :src="logoURL" alt class="logo me-2" :class="logoClass" @load="statusPageLogoLoaded" /> <img :src="logoURL" alt class="logo me-2" :class="logoClass" />
<font-awesome-icon v-if="enableEditMode" class="icon-upload" icon="upload" /> <font-awesome-icon v-if="enableEditMode" class="icon-upload" icon="upload" />
</span> </span>
@ -538,7 +538,7 @@ export default {
this.slug = "default"; this.slug = "default";
} }
axios.get("/api/status-page/" + this.slug).then((res) => { this.getData().then((res) => {
this.config = res.data.config; this.config = res.data.config;
if (!this.config.domainNameList) { if (!this.config.domainNameList) {
@ -551,6 +551,11 @@ export default {
this.incident = res.data.incident; this.incident = res.data.incident;
this.$root.publicGroupList = res.data.publicGroupList; this.$root.publicGroupList = res.data.publicGroupList;
}).catch( function (error) {
if (error.response.status === 404) {
location.href = "/page-not-found";
}
console.log(error);
}); });
// 5mins a loop // 5mins a loop
@ -567,6 +572,21 @@ export default {
}, },
methods: { methods: {
/**
* Get status page data
* It should be preloaded in window.preloadData
* @returns {Promise<any>}
*/
getData: function () {
if (window.preloadData) {
return new Promise(resolve => resolve({
data: window.preloadData
}));
} else {
return axios.get("/api/status-page/" + this.slug);
}
},
highlighter(code) { highlighter(code) {
return highlight(code, languages.css); return highlight(code, languages.css);
}, },
@ -604,6 +624,9 @@ export default {
this.$root.initSocketIO(true); this.$root.initSocketIO(true);
this.enableEditMode = true; this.enableEditMode = true;
this.clickedEditButton = true; this.clickedEditButton = true;
// Try to fix #1658
this.loadedData = true;
} }
}, },
@ -687,11 +710,6 @@ export default {
} }
}, },
statusPageLogoLoaded(eventPayload) {
// Remark: may not work in dev, due to CORS
favicon.image(eventPayload.target);
},
createIncident() { createIncident() {
this.enableEditIncidentMode = true; this.enableEditIncidentMode = true;

View File

@ -1,4 +1,5 @@
import { createRouter, createWebHistory } from "vue-router"; import { createRouter, createWebHistory } from "vue-router";
import EmptyLayout from "./layouts/EmptyLayout.vue"; import EmptyLayout from "./layouts/EmptyLayout.vue";
import Layout from "./layouts/Layout.vue"; import Layout from "./layouts/Layout.vue";
import Dashboard from "./pages/Dashboard.vue"; import Dashboard from "./pages/Dashboard.vue";
@ -8,22 +9,23 @@ import EditMonitor from "./pages/EditMonitor.vue";
import List from "./pages/List.vue"; import List from "./pages/List.vue";
const Settings = () => import("./pages/Settings.vue"); const Settings = () => import("./pages/Settings.vue");
import Setup from "./pages/Setup.vue"; import Setup from "./pages/Setup.vue";
const StatusPage = () => import("./pages/StatusPage.vue"); import StatusPage from "./pages/StatusPage.vue";
import Entry from "./pages/Entry.vue"; import Entry from "./pages/Entry.vue";
import Appearance from "./components/settings/Appearance.vue";
import General from "./components/settings/General.vue";
import Notifications from "./components/settings/Notifications.vue";
import ReverseProxy from "./components/settings/ReverseProxy.vue";
import MonitorHistory from "./components/settings/MonitorHistory.vue";
import Security from "./components/settings/Security.vue";
import Proxies from "./components/settings/Proxies.vue";
import Backup from "./components/settings/Backup.vue";
import About from "./components/settings/About.vue";
import ManageStatusPage from "./pages/ManageStatusPage.vue"; import ManageStatusPage from "./pages/ManageStatusPage.vue";
import AddStatusPage from "./pages/AddStatusPage.vue"; import AddStatusPage from "./pages/AddStatusPage.vue";
import NotFound from "./pages/NotFound.vue"; import NotFound from "./pages/NotFound.vue";
// Settings - Sub Pages
import Appearance from "./components/settings/Appearance.vue";
import General from "./components/settings/General.vue";
const Notifications = () => import("./components/settings/Notifications.vue");
import ReverseProxy from "./components/settings/ReverseProxy.vue";
import MonitorHistory from "./components/settings/MonitorHistory.vue";
const Security = () => import("./components/settings/Security.vue");
import Proxies from "./components/settings/Proxies.vue";
import Backup from "./components/settings/Backup.vue";
import About from "./components/settings/About.vue";
const routes = [ const routes = [
{ {
path: "/", path: "/",
@ -63,12 +65,12 @@ const routes = [
path: "/add", path: "/add",
component: EditMonitor, component: EditMonitor,
}, },
{
path: "/list",
component: List,
},
], ],
}, },
{
path: "/list",
component: List,
},
{ {
path: "/settings", path: "/settings",
component: Settings, component: Settings,

View File

@ -159,7 +159,6 @@ describe("Test genSecret", () => {
expect(secret).toContain("A"); expect(secret).toContain("A");
expect(secret).toContain("9"); expect(secret).toContain("9");
}); });
}); });
describe("Test reset-password", () => { describe("Test reset-password", () => {
@ -169,6 +168,9 @@ describe("Test reset-password", () => {
}); });
describe("Test Discord Notification Provider", () => { describe("Test Discord Notification Provider", () => {
const hostname = "discord.com";
const port = 1337;
const sendNotification = async (hostname, port, type) => { const sendNotification = async (hostname, port, type) => {
const discordProvider = new Discord(); const discordProvider = new Discord();
@ -191,63 +193,35 @@ describe("Test Discord Notification Provider", () => {
); );
}; };
it("should send hostname for dns monitors", async () => {
const hostname = "discord.com";
await sendNotification(hostname, null, "dns");
expect(axios.post.mock.lastCall[1].embeds[0].fields[1].value).toBe(
hostname
);
});
it("should send hostname for ping monitors", async () => { it("should send hostname for ping monitors", async () => {
const hostname = "discord.com";
await sendNotification(hostname, null, "ping"); await sendNotification(hostname, null, "ping");
expect(axios.post.mock.lastCall[1].embeds[0].fields[1].value).toBe(hostname);
expect(axios.post.mock.lastCall[1].embeds[0].fields[1].value).toBe(
hostname
);
}); });
it("should send hostname for port monitors", async () => { it.each([ "dns", "port", "steam" ])("should send hostname for %p monitors", async (type) => {
const hostname = "discord.com"; await sendNotification(hostname, port, type);
const port = 1337; expect(axios.post.mock.lastCall[1].embeds[0].fields[1].value).toBe(`${hostname}:${port}`);
await sendNotification(hostname, port, "port");
expect(axios.post.mock.lastCall[1].embeds[0].fields[1].value).toBe(
`${hostname}:${port}`
);
});
it("should send hostname for steam monitors", async () => {
const hostname = "discord.com";
const port = 1337;
await sendNotification(hostname, port, "steam");
expect(axios.post.mock.lastCall[1].embeds[0].fields[1].value).toBe(
`${hostname}:${port}`
);
}); });
}); });
describe("The function filterAndJoin", () => { describe("The function filterAndJoin", () => {
it("should join and array of strings to one string", () => { it("should join and array of strings to one string", () => {
const result = utilServerRewire.filterAndJoin(["one", "two", "three"]); const result = utilServerRewire.filterAndJoin([ "one", "two", "three" ]);
expect(result).toBe("onetwothree"); expect(result).toBe("onetwothree");
}); });
it("should join strings using a given connector", () => { it("should join strings using a given connector", () => {
const result = utilServerRewire.filterAndJoin(["one", "two", "three"], "-"); const result = utilServerRewire.filterAndJoin([ "one", "two", "three" ], "-");
expect(result).toBe("one-two-three"); expect(result).toBe("one-two-three");
}); });
it("should filter null, undefined and empty strings before joining", () => { it("should filter null, undefined and empty strings before joining", () => {
const result = utilServerRewire.filterAndJoin([undefined, "", "three"], "--"); const result = utilServerRewire.filterAndJoin([ undefined, "", "three" ], "--");
expect(result).toBe("three"); expect(result).toBe("three");
}); });
it("should return an empty string if all parts are filtered out", () => { it("should return an empty string if all parts are filtered out", () => {
const result = utilServerRewire.filterAndJoin([undefined, "", ""], "--"); const result = utilServerRewire.filterAndJoin([ undefined, "", "" ], "--");
expect(result).toBe(""); expect(result).toBe("");
}); });
}); });