diff --git a/.devcontainer/README.md b/.devcontainer/README.md
new file mode 100644
index 00000000..4a34b211
--- /dev/null
+++ b/.devcontainer/README.md
@@ -0,0 +1,28 @@
+# Codespaces
+
+You can modifiy Uptime Kuma in your browser without setting up a local development.
+
+![image](https://github.com/louislam/uptime-kuma/assets/1336778/31d9f06d-dd0b-4405-8e0d-a96586ee4595)
+
+1. Click `Code` -> `Create codespace on master`
+2. Wait a few minutes until you see there are two exposed ports
+3. Go to the `3000` url, see if it is working
+
+![image](https://github.com/louislam/uptime-kuma/assets/1336778/909b2eb4-4c5e-44e4-ac26-6d20ed856e7f)
+
+## Frontend
+
+Since the frontend is using [Vite.js](https://vitejs.dev/), all changes in this area will be hot-reloaded.
+You don't need to restart the frontend, unless you try to add a new frontend dependency.
+
+## Backend
+
+The backend does not automatically hot-reload.
+You will need to restart the backend after changing something using these steps:
+
+1. Click `Terminal`
+2. Click `Codespaces: server-dev` in the right panel
+3. Press `Ctrl + C` to stop the server
+4. Press `Up` to run `npm run start-server-dev`
+
+![image](https://github.com/louislam/uptime-kuma/assets/1336778/e0c0a350-fe46-4588-9f37-e053c85834d1)
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 00000000..5b3ceabc
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,22 @@
+{
+ "image": "mcr.microsoft.com/devcontainers/javascript-node:dev-18-bookworm",
+ "features": {
+ "ghcr.io/devcontainers/features/github-cli:1": {}
+ },
+ "updateContentCommand": "npm ci",
+ "postCreateCommand": "",
+ "postAttachCommand": {
+ "frontend-dev": "npm run start-frontend-devcontainer",
+ "server-dev": "npm run start-server-dev",
+ "open-port": "gh codespace ports visibility 3001:public -c $CODESPACE_NAME"
+ },
+ "customizations": {
+ "vscode": {
+ "extensions": [
+ "streetsidesoftware.code-spell-checker",
+ "dbaeumer.vscode-eslint"
+ ]
+ }
+ },
+ "forwardPorts": [3000, 3001]
+}
diff --git a/.github/ISSUE_TEMPLATE/ask-for-help.yaml b/.github/ISSUE_TEMPLATE/ask-for-help.yaml
index 9c30b2dc..c082b2e3 100644
--- a/.github/ISSUE_TEMPLATE/ask-for-help.yaml
+++ b/.github/ISSUE_TEMPLATE/ask-for-help.yaml
@@ -44,7 +44,7 @@ body:
id: operating-system
attributes:
label: "💻 Operating System and Arch"
- description: "Which OS is your server/device running on?"
+ description: "Which OS is your server/device running on? (For Replit, please do not report this bug)"
placeholder: "Ex. Ubuntu 20.04 x86"
validations:
required: true
@@ -52,7 +52,7 @@ body:
id: browser-vendor
attributes:
label: "🌐 Browser"
- description: "Which browser are you running on?"
+ description: "Which browser are you running on? (For Replit, please do not report this bug)"
placeholder: "Ex. Google Chrome 95.0.4638.69"
validations:
required: true
diff --git a/README.md b/README.md
index c7aa4150..0e41652d 100644
--- a/README.md
+++ b/README.md
@@ -54,7 +54,7 @@ Requirements:
- ✅ Major Linux distros such as Debian, Ubuntu, CentOS, Fedora and ArchLinux etc.
- ✅ Windows 10 (x64), Windows Server 2012 R2 (x64) or higher
- ❌ Replit / Heroku
-- [Node.js](https://nodejs.org/en/download/) 14 / 16 / 18 (20 is not supported)
+- [Node.js](https://nodejs.org/en/download/) 14 / 16 / 18 / 20.4
- [npm](https://docs.npmjs.com/cli/) >= 7
- [Git](https://git-scm.com/downloads)
- [pm2](https://pm2.keymetrics.io/) - For running Uptime Kuma in the background
diff --git a/config/vite.config.js b/config/vite.config.js
index 6e9ebbde..11c61006 100644
--- a/config/vite.config.js
+++ b/config/vite.config.js
@@ -3,6 +3,7 @@ import vue from "@vitejs/plugin-vue";
import { defineConfig } from "vite";
import visualizer from "rollup-plugin-visualizer";
import viteCompression from "vite-plugin-compression";
+import commonjs from "vite-plugin-commonjs";
const postCssScss = require("postcss-scss");
const postcssRTLCSS = require("postcss-rtlcss");
@@ -16,8 +17,12 @@ export default defineConfig({
},
define: {
"FRONTEND_VERSION": JSON.stringify(process.env.npm_package_version),
+ "DEVCONTAINER": JSON.stringify(process.env.DEVCONTAINER),
+ "GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN": JSON.stringify(process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN),
+ "CODESPACE_NAME": JSON.stringify(process.env.CODESPACE_NAME),
},
plugins: [
+ commonjs(),
vue(),
legacy({
targets: [ "since 2015" ],
@@ -42,6 +47,9 @@ export default defineConfig({
}
},
build: {
+ commonjsOptions: {
+ include: [ /.js$/ ],
+ },
rollupOptions: {
output: {
manualChunks(id, { getModuleInfo, getModuleIds }) {
diff --git a/db/patch-added-kafka-producer.sql b/db/patch-added-kafka-producer.sql
new file mode 100644
index 00000000..933d30b8
--- /dev/null
+++ b/db/patch-added-kafka-producer.sql
@@ -0,0 +1,22 @@
+-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
+BEGIN TRANSACTION;
+
+ALTER TABLE monitor
+ ADD kafka_producer_topic VARCHAR(255);
+
+ALTER TABLE monitor
+ ADD kafka_producer_brokers TEXT;
+
+ALTER TABLE monitor
+ ADD kafka_producer_ssl INTEGER;
+
+ALTER TABLE monitor
+ ADD kafka_producer_allow_auto_topic_creation VARCHAR(255);
+
+ALTER TABLE monitor
+ ADD kafka_producer_sasl_options TEXT;
+
+ALTER TABLE monitor
+ ADD kafka_producer_message TEXT;
+
+COMMIT;
diff --git a/docker/dockerfile b/docker/dockerfile
index 239a0c95..1bc90f92 100644
--- a/docker/dockerfile
+++ b/docker/dockerfile
@@ -72,7 +72,6 @@ RUN git clone https://github.com/louislam/uptime-kuma.git .
RUN npm ci
EXPOSE 3000 3001
-VOLUME ["/app/data"]
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD extra/healthcheck
CMD ["npm", "run", "start-pr-test"]
diff --git a/package-lock.json b/package-lock.json
index ad26942e..62c5adb0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,7 @@
"license": "MIT",
"dependencies": {
"@grpc/grpc-js": "~1.7.3",
- "@louislam/ping": "~0.4.4-mod.0",
+ "@louislam/ping": "~0.4.4-mod.1",
"@louislam/sqlite3": "15.1.6",
"args-parser": "~1.3.0",
"axios": "~0.27.0",
@@ -41,6 +41,7 @@
"jsonata": "^2.0.3",
"jsonwebtoken": "~9.0.0",
"jwt-decode": "~3.1.2",
+ "kafkajs": "^2.2.4",
"limiter": "~2.1.0",
"liquidjs": "^10.7.0",
"mongodb": "~4.14.0",
@@ -62,6 +63,7 @@
"qs": "~6.10.4",
"redbean-node": "~0.3.0",
"redis": "~4.5.1",
+ "semver": "~7.5.4",
"socket.io": "~4.6.1",
"socket.io-client": "~4.6.1",
"socks-proxy-agent": "6.1.1",
@@ -116,6 +118,7 @@
"typescript": "~4.4.4",
"v-pagination-3": "~0.1.7",
"vite": "~4.4.1",
+ "vite-plugin-commonjs": "^0.8.0",
"vite-plugin-compression": "^0.5.1",
"vue": "~3.3.4",
"vue-chartjs": "~5.2.0",
@@ -133,7 +136,7 @@
"whatwg-url": "~12.0.1"
},
"engines": {
- "node": "14.* || 16.* || 18.*"
+ "node": "14 || 16 || 18 || >= 20.4.0"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -4395,11 +4398,12 @@
"dev": true
},
"node_modules/@louislam/ping": {
- "version": "0.4.4-mod.0",
- "resolved": "https://registry.npmjs.org/@louislam/ping/-/ping-0.4.4-mod.0.tgz",
- "integrity": "sha512-U2ZXcgFRPmZYd/ooA8KILG4aCMBsDrGP9NDWseHriZSsKlu5Y1lf/LbenN6tnqQ9JjAsbJjqwSi3xtAcWqU+1w==",
+ "version": "0.4.4-mod.1",
+ "resolved": "https://registry.npmjs.org/@louislam/ping/-/ping-0.4.4-mod.1.tgz",
+ "integrity": "sha512-uMq6qwL9/VYh2YBbDEhM7ZzJ8YON+juw/3k+28P3s9ue3uDMQ56MNPfywXoRpsxkU8RgjN0TDzEhQDzO1WisMw==",
"dependencies": {
- "command-exists": "~1.2.9"
+ "command-exists": "~1.2.9",
+ "underscore": "~1.13.6"
},
"engines": {
"node": ">=4.0.0"
@@ -8751,6 +8755,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/es-module-lexer": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz",
+ "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==",
+ "dev": true
+ },
"node_modules/es-set-tostringtag": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz",
@@ -13001,6 +13011,14 @@
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
},
+ "node_modules/kafkajs": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/kafkajs/-/kafkajs-2.2.4.tgz",
+ "integrity": "sha512-j/YeapB1vfPT2iOIUn/vxdyKEuhuY2PxMBvf5JWux6iSaukAccrMtXEY/Lb7OvavDhOWME589bpLrEdnVHjfjA==",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/keyv": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz",
@@ -17741,6 +17759,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/underscore": {
+ "version": "1.13.6",
+ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz",
+ "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A=="
+ },
"node_modules/unicode-canonical-property-names-ecmascript": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz",
@@ -18020,6 +18043,18 @@
}
}
},
+ "node_modules/vite-plugin-commonjs": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/vite-plugin-commonjs/-/vite-plugin-commonjs-0.8.1.tgz",
+ "integrity": "sha512-hL2wwqgSiLBcrmCH7z+H468Z9uyBnKXX5OAwoYmWd/i03PBGCqkOBR3rjeojyWOoGmWgDVB7lj6Xn5pVw3Fwyg==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^8.8.2",
+ "fast-glob": "^3.2.12",
+ "magic-string": "^0.30.1",
+ "vite-plugin-dynamic-import": "^1.5.0"
+ }
+ },
"node_modules/vite-plugin-compression": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/vite-plugin-compression/-/vite-plugin-compression-0.5.1.tgz",
@@ -18118,6 +18153,18 @@
"node": ">=8"
}
},
+ "node_modules/vite-plugin-dynamic-import": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/vite-plugin-dynamic-import/-/vite-plugin-dynamic-import-1.5.0.tgz",
+ "integrity": "sha512-Qp85c+AVJmLa8MLni74U4BDiWpUeFNx7NJqbGZyR2XJOU7mgW0cb7nwlAMucFyM4arEd92Nfxp4j44xPi6Fu7g==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^8.8.2",
+ "es-module-lexer": "^1.2.1",
+ "fast-glob": "^3.2.12",
+ "magic-string": "^0.30.1"
+ }
+ },
"node_modules/vue": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.3.4.tgz",
diff --git a/package.json b/package.json
index fadd0f0d..d19f01f1 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
"url": "https://github.com/louislam/uptime-kuma.git"
},
"engines": {
- "node": "14.* || 16.* || 18.*"
+ "node": "14 || 16 || 18 || >= 20.4.0"
},
"scripts": {
"install-legacy": "npm install",
@@ -19,6 +19,7 @@
"lint": "npm run lint:js && npm run lint:style",
"dev": "concurrently -k -r \"wait-on tcp:3000 && npm run start-server-dev \" \"npm run start-frontend-dev\"",
"start-frontend-dev": "cross-env NODE_ENV=development vite --host --config ./config/vite.config.js",
+ "start-frontend-devcontainer": "cross-env NODE_ENV=development DEVCONTAINER=1 vite --host --config ./config/vite.config.js",
"start": "npm run start-server",
"start-server": "node server/server.js",
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
@@ -69,7 +70,7 @@
},
"dependencies": {
"@grpc/grpc-js": "~1.7.3",
- "@louislam/ping": "~0.4.4-mod.0",
+ "@louislam/ping": "~0.4.4-mod.1",
"@louislam/sqlite3": "15.1.6",
"args-parser": "~1.3.0",
"axios": "~0.27.0",
@@ -100,6 +101,7 @@
"jsonata": "^2.0.3",
"jsonwebtoken": "~9.0.0",
"jwt-decode": "~3.1.2",
+ "kafkajs": "^2.2.4",
"limiter": "~2.1.0",
"liquidjs": "^10.7.0",
"mongodb": "~4.14.0",
@@ -121,6 +123,7 @@
"qs": "~6.10.4",
"redbean-node": "~0.3.0",
"redis": "~4.5.1",
+ "semver": "~7.5.4",
"socket.io": "~4.6.1",
"socket.io-client": "~4.6.1",
"socks-proxy-agent": "6.1.1",
@@ -175,6 +178,7 @@
"typescript": "~4.4.4",
"v-pagination-3": "~0.1.7",
"vite": "~4.4.1",
+ "vite-plugin-commonjs": "^0.8.0",
"vite-plugin-compression": "^0.5.1",
"vue": "~3.3.4",
"vue-chartjs": "~5.2.0",
diff --git a/server/client.js b/server/client.js
index 3efbe8fd..2e3bd43b 100644
--- a/server/client.js
+++ b/server/client.js
@@ -141,12 +141,21 @@ async function sendAPIKeyList(socket) {
/**
* Emits the version information to the client.
* @param {Socket} socket Socket.io socket instance
+ * @param {boolean} hideVersion
* @returns {Promise}
*/
-async function sendInfo(socket) {
+async function sendInfo(socket, hideVersion = false) {
+ let version;
+ let latestVersion;
+
+ if (!hideVersion) {
+ version = checkVersion.version;
+ latestVersion = checkVersion.latestVersion;
+ }
+
socket.emit("info", {
- version: checkVersion.version,
- latestVersion: checkVersion.latestVersion,
+ version,
+ latestVersion,
primaryBaseURL: await setting("primaryBaseURL"),
serverTimezone: await server.getTimezone(),
serverTimezoneOffset: server.getTimezoneOffset(),
diff --git a/server/config.js b/server/config.js
index 43a40f67..77f9e74b 100644
--- a/server/config.js
+++ b/server/config.js
@@ -1,4 +1,5 @@
-const args = require("args-parser")(process.argv);
+// Interop with browser
+const args = (typeof process !== "undefined") ? require("args-parser")(process.argv) : {};
const demoMode = args["demo"] || false;
const badgeConstants = {
diff --git a/server/database.js b/server/database.js
index c283a55b..7b1d9f93 100644
--- a/server/database.js
+++ b/server/database.js
@@ -73,6 +73,7 @@ class Database {
"patch-add-parent-monitor.sql": true,
"patch-add-invert-keyword.sql": true,
"patch-added-json-query.sql": true,
+ "patch-added-kafka-producer.sql": true,
"patch-add-certificate-expiry-status-page.sql": true,
};
diff --git a/server/model/monitor.js b/server/model/monitor.js
index 4d760d36..f28b4fe2 100644
--- a/server/model/monitor.js
+++ b/server/model/monitor.js
@@ -6,7 +6,7 @@ const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVA
SQL_DATETIME_FORMAT
} = require("../../src/util");
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery,
- redisPingAsync, mongodbPing,
+ redisPingAsync, mongodbPing, kafkaProducerAsync
} = require("../util-server");
const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model");
@@ -137,6 +137,11 @@ class Monitor extends BeanModel {
httpBodyEncoding: this.httpBodyEncoding,
jsonPath: this.jsonPath,
expectedValue: this.expectedValue,
+ kafkaProducerTopic: this.kafkaProducerTopic,
+ kafkaProducerBrokers: JSON.parse(this.kafkaProducerBrokers),
+ kafkaProducerSsl: this.kafkaProducerSsl === "1" && true || false,
+ kafkaProducerAllowAutoTopicCreation: this.kafkaProducerAllowAutoTopicCreation === "1" && true || false,
+ kafkaProducerMessage: this.kafkaProducerMessage,
screenshot,
};
@@ -161,6 +166,7 @@ class Monitor extends BeanModel {
tlsCa: this.tlsCa,
tlsCert: this.tlsCert,
tlsKey: this.tlsKey,
+ kafkaProducerSaslOptions: JSON.parse(this.kafkaProducerSaslOptions),
};
}
@@ -175,7 +181,7 @@ class Monitor extends BeanModel {
async isActive() {
const parentActive = await Monitor.isParentActive(this.id);
- return this.active && parentActive;
+ return (this.active === 1) && parentActive;
}
/**
@@ -825,6 +831,24 @@ class Monitor extends BeanModel {
bean.ping = dayjs().valueOf() - startTime;
}
+ } else if (this.type === "kafka-producer") {
+ let startTime = dayjs().valueOf();
+
+ bean.msg = await kafkaProducerAsync(
+ JSON.parse(this.kafkaProducerBrokers),
+ this.kafkaProducerTopic,
+ this.kafkaProducerMessage,
+ {
+ allowAutoTopicCreation: this.kafkaProducerAllowAutoTopicCreation,
+ ssl: this.kafkaProducerSsl,
+ clientId: `Uptime-Kuma/${version}`,
+ interval: this.interval,
+ },
+ JSON.parse(this.kafkaProducerSaslOptions),
+ );
+ bean.status = UP;
+ bean.ping = dayjs().valueOf() - startTime;
+
} else {
throw new Error("Unknown Monitor Type");
}
diff --git a/server/notification-providers/slack.js b/server/notification-providers/slack.js
index 12207bd4..41c2bd02 100644
--- a/server/notification-providers/slack.js
+++ b/server/notification-providers/slack.js
@@ -27,6 +27,11 @@ class Slack extends NotificationProvider {
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
+
+ if (notification.slackchannelnotify) {
+ msg += " ";
+ }
+
try {
if (heartbeatJSON == null) {
let data = {
@@ -53,7 +58,7 @@ class Slack extends NotificationProvider {
"type": "header",
"text": {
"type": "plain_text",
- "text": "Uptime Kuma Alert",
+ "text": textMsg,
},
},
{
diff --git a/server/notification-providers/smsc.js b/server/notification-providers/smsc.js
new file mode 100644
index 00000000..251bc455
--- /dev/null
+++ b/server/notification-providers/smsc.js
@@ -0,0 +1,42 @@
+const NotificationProvider = require("./notification-provider");
+const axios = require("axios");
+
+class SMSC extends NotificationProvider {
+ name = "smsc";
+
+ async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
+ let okMsg = "Sent Successfully.";
+ try {
+ let config = {
+ headers: {
+ "Content-Type": "application/json",
+ "Accept": "text/json",
+ }
+ };
+
+ let getArray = [
+ "fmt=3",
+ "translit=" + notification.smscTranslit,
+ "login=" + notification.smscLogin,
+ "psw=" + notification.smscPassword,
+ "phones=" + notification.smscToNumber,
+ "mes=" + encodeURIComponent(msg.replace(/[^\x00-\x7F]/g, "")),
+ ];
+ if (notification.smscSenderName !== "") {
+ getArray.push("sender=" + notification.smscSenderName);
+ }
+
+ let resp = await axios.get("https://smsc.kz/sys/send.php?" + getArray.join("&"), config);
+ if (resp.data.id === undefined) {
+ let error = `Something gone wrong. Api returned code ${resp.data.error_code}: ${resp.data.error}`;
+ this.throwGeneralAxiosError(error);
+ }
+
+ return okMsg;
+ } catch (error) {
+ this.throwGeneralAxiosError(error);
+ }
+ }
+}
+
+module.exports = SMSC;
diff --git a/server/notification.js b/server/notification.js
index 9bfa371d..ea5c8ee0 100644
--- a/server/notification.js
+++ b/server/notification.js
@@ -6,6 +6,7 @@ const AliyunSms = require("./notification-providers/aliyun-sms");
const Apprise = require("./notification-providers/apprise");
const Bark = require("./notification-providers/bark");
const ClickSendSMS = require("./notification-providers/clicksendsms");
+const SMSC = require("./notification-providers/smsc");
const DingDing = require("./notification-providers/dingding");
const Discord = require("./notification-providers/discord");
const Feishu = require("./notification-providers/feishu");
@@ -68,6 +69,7 @@ class Notification {
new Apprise(),
new Bark(),
new ClickSendSMS(),
+ new SMSC(),
new DingDing(),
new Discord(),
new Feishu(),
diff --git a/server/routers/api-router.js b/server/routers/api-router.js
index 8b5d36f2..f51f046d 100644
--- a/server/routers/api-router.js
+++ b/server/routers/api-router.js
@@ -442,7 +442,7 @@ router.get("/api/badge/:id/cert-exp", cache("5 minutes"), async (request, respon
if (!tlsInfo.valid) {
// return a "Bad Cert" badge in naColor (grey), when cert is not valid
badgeValues.message = "Bad Cert";
- badgeValues.color = badgeConstants.downColor;
+ badgeValues.color = downColor;
} else {
const daysRemaining = parseInt(overrideValue ?? tlsInfo.certInfo.daysRemaining);
diff --git a/server/server.js b/server/server.js
index b9d618f5..5f4ccc46 100644
--- a/server/server.js
+++ b/server/server.js
@@ -15,20 +15,27 @@ dayjs.extend(require("dayjs/plugin/customParseFormat"));
require("dotenv").config();
// Check Node.js Version
-const nodeVersion = parseInt(process.versions.node.split(".")[0]);
-const requiredVersion = 14;
+const nodeVersion = process.versions.node;
+
+// Get the required Node.js version from package.json
+const requiredNodeVersions = require("../package.json").engines.node;
+const bannedNodeVersions = " < 14 || 20.0.* || 20.1.* || 20.2.* || 20.3.* ";
console.log(`Your Node.js version: ${nodeVersion}`);
-// See more: https://github.com/louislam/uptime-kuma/issues/3138
-if (nodeVersion >= 20) {
- console.warn("\x1b[31m%s\x1b[0m", "Warning: Uptime Kuma is currently not stable on Node.js >= 20, please use Node.js 18.");
-}
+const semver = require("semver");
+const requiredNodeVersionsComma = requiredNodeVersions.split("||").map((version) => version.trim()).join(", ");
-if (nodeVersion < requiredVersion) {
- console.error(`Error: Your Node.js version is not supported, please upgrade to Node.js >= ${requiredVersion}.`);
+// Exit Uptime Kuma immediately if the Node.js version is banned
+if (semver.satisfies(nodeVersion, bannedNodeVersions)) {
+ console.error("\x1b[31m%s\x1b[0m", `Error: Your Node.js version: ${nodeVersion} is not supported, please upgrade your Node.js to ${requiredNodeVersionsComma}.`);
process.exit(-1);
}
+// Warning if the Node.js version is not in the support list, but it maybe still works
+if (!semver.satisfies(nodeVersion, requiredNodeVersions)) {
+ console.warn("\x1b[31m%s\x1b[0m", `Warning: Your Node.js version: ${nodeVersion} is not officially supported, please upgrade your Node.js to ${requiredNodeVersionsComma}.`);
+}
+
const args = require("args-parser")(process.argv);
const { sleep, log, getRandomInt, genSecret, isDev } = require("../src/util");
const config = require("./config");
@@ -263,7 +270,7 @@ let needSetup = false;
log.info("server", "Adding socket handler");
io.on("connection", async (socket) => {
- sendInfo(socket);
+ sendInfo(socket, true);
if (needSetup) {
log.info("server", "Redirect to setup page");
@@ -636,6 +643,9 @@ let needSetup = false;
monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
delete monitor.accepted_statuscodes;
+ monitor.kafkaProducerBrokers = JSON.stringify(monitor.kafkaProducerBrokers);
+ monitor.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions);
+
bean.import(monitor);
bean.user_id = socket.userID;
@@ -750,6 +760,11 @@ let needSetup = false;
bean.httpBodyEncoding = monitor.httpBodyEncoding;
bean.expectedValue = monitor.expectedValue;
bean.jsonPath = monitor.jsonPath;
+ bean.kafkaProducerTopic = monitor.kafkaProducerTopic;
+ bean.kafkaProducerBrokers = JSON.stringify(monitor.kafkaProducerBrokers);
+ bean.kafkaProducerAllowAutoTopicCreation = monitor.kafkaProducerAllowAutoTopicCreation;
+ bean.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions);
+ bean.kafkaProducerMessage = monitor.kafkaProducerMessage;
bean.validate();
@@ -1651,6 +1666,7 @@ async function afterLogin(socket, user) {
socket.join(user.id);
let monitorList = await server.sendMonitorList(socket);
+ sendInfo(socket);
server.sendMaintenanceList(socket);
sendNotificationList(socket);
sendProxyList(socket);
diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js
index b45a749b..da86f3b9 100644
--- a/server/uptime-kuma-server.js
+++ b/server/uptime-kuma-server.js
@@ -10,7 +10,7 @@ const util = require("util");
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
const { Settings } = require("./settings");
const dayjs = require("dayjs");
-// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`
+// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`, put at the bottom of this file instead.
/**
* `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
@@ -249,9 +249,9 @@ class UptimeKumaServer {
return (typeof forwardedFor === "string" ? forwardedFor.split(",")[0].trim() : null)
|| socket.client.conn.request.headers["x-real-ip"]
- || clientIP.replace(/^.*:/, "");
+ || clientIP.replace(/^::ffff:/, "");
} else {
- return clientIP.replace(/^.*:/, "");
+ return clientIP.replace(/^::ffff:/, "");
}
}
@@ -262,13 +262,43 @@ class UptimeKumaServer {
* @returns {Promise}
*/
async getTimezone() {
+ // From process.env.TZ
+ try {
+ if (process.env.TZ) {
+ this.checkTimezone(process.env.TZ);
+ return process.env.TZ;
+ }
+ } catch (e) {
+ log.warn("timezone", e.message + " in process.env.TZ");
+ }
+
let timezone = await Settings.get("serverTimezone");
- if (timezone) {
- return timezone;
- } else if (process.env.TZ) {
- return process.env.TZ;
- } else {
- return dayjs.tz.guess();
+
+ // From Settings
+ try {
+ log.debug("timezone", "Using timezone from settings: " + timezone);
+ if (timezone) {
+ this.checkTimezone(timezone);
+ return timezone;
+ }
+ } catch (e) {
+ log.warn("timezone", e.message + " in settings");
+ }
+
+ // Guess
+ try {
+ let guess = dayjs.tz.guess();
+ log.debug("timezone", "Guessing timezone: " + guess);
+ if (guess) {
+ this.checkTimezone(guess);
+ return guess;
+ } else {
+ return "UTC";
+ }
+ } catch (e) {
+ // Guess failed, fall back to UTC
+ log.debug("timezone", "Guessed an invalid timezone. Use UTC as fallback");
+ return "UTC";
}
}
@@ -280,11 +310,24 @@ class UptimeKumaServer {
return dayjs().format("Z");
}
+ /**
+ * Throw an error if the timezone is invalid
+ * @param timezone
+ */
+ checkTimezone(timezone) {
+ try {
+ dayjs.utc("2013-11-18 11:55").tz(timezone).format();
+ } catch (e) {
+ throw new Error("Invalid timezone:" + timezone);
+ }
+ }
+
/**
* Set the current server timezone and environment variables
* @param {string} timezone
*/
async setTimezone(timezone) {
+ this.checkTimezone(timezone);
await Settings.set("serverTimezone", timezone, "general");
process.env.TZ = timezone;
dayjs.tz.setDefault(timezone);
@@ -300,6 +343,5 @@ module.exports = {
UptimeKumaServer
};
-// Must be at the end
-const { MonitorType } = require("./monitor-types/monitor-type");
+// Must be at the end to avoid circular dependencies
const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor-type");
diff --git a/server/util-server.js b/server/util-server.js
index 4ddb6ce3..031d8b67 100644
--- a/server/util-server.js
+++ b/server/util-server.js
@@ -28,8 +28,11 @@ const {
} = require("node-radius-utils");
const dayjs = require("dayjs");
-const isWindows = process.platform === /^win/.test(process.platform);
+// SASLOptions used in JSDoc
+// eslint-disable-next-line no-unused-vars
+const { Kafka, SASLOptions } = require("kafkajs");
+const isWindows = process.platform === /^win/.test(process.platform);
/**
* Init or reset JWT secret
* @returns {Promise}
@@ -196,6 +199,94 @@ exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
});
};
+/**
+ * Monitor Kafka using Producer
+ * @param {string} topic Topic name to produce into
+ * @param {string} message Message to produce
+ * @param {Object} [options={interval = 20, allowAutoTopicCreation = false, ssl = false, clientId = "Uptime-Kuma"}]
+ * Kafka client options. Contains ssl, clientId, allowAutoTopicCreation and
+ * interval (interval defaults to 20, allowAutoTopicCreation defaults to false, clientId defaults to "Uptime-Kuma"
+ * and ssl defaults to false)
+ * @param {string[]} brokers List of kafka brokers to connect, host and port joined by ':'
+ * @param {SASLOptions} [saslOptions={}] Options for kafka client Authentication (SASL) (defaults to
+ * {})
+ * @returns {Promise}
+ */
+exports.kafkaProducerAsync = function (brokers, topic, message, options = {}, saslOptions = {}) {
+ return new Promise((resolve, reject) => {
+ const { interval = 20, allowAutoTopicCreation = false, ssl = false, clientId = "Uptime-Kuma" } = options;
+
+ let connectedToKafka = false;
+
+ const timeoutID = setTimeout(() => {
+ log.debug("kafkaProducer", "KafkaProducer timeout triggered");
+ connectedToKafka = true;
+ reject(new Error("Timeout"));
+ }, interval * 1000 * 0.8);
+
+ if (saslOptions.mechanism === "None") {
+ saslOptions = undefined;
+ }
+
+ let client = new Kafka({
+ brokers: brokers,
+ clientId: clientId,
+ sasl: saslOptions,
+ retry: {
+ retries: 0,
+ },
+ ssl: ssl,
+ });
+
+ let producer = client.producer({
+ allowAutoTopicCreation: allowAutoTopicCreation,
+ retry: {
+ retries: 0,
+ }
+ });
+
+ producer.connect().then(
+ () => {
+ try {
+ producer.send({
+ topic: topic,
+ messages: [{
+ value: message,
+ }],
+ });
+ connectedToKafka = true;
+ clearTimeout(timeoutID);
+ resolve("Message sent successfully");
+ } catch (e) {
+ connectedToKafka = true;
+ producer.disconnect();
+ clearTimeout(timeoutID);
+ reject(new Error("Error sending message: " + e.message));
+ }
+ }
+ ).catch(
+ (e) => {
+ connectedToKafka = true;
+ producer.disconnect();
+ clearTimeout(timeoutID);
+ reject(new Error("Error in producer connection: " + e.message));
+ }
+ );
+
+ producer.on("producer.network.request_timeout", (_) => {
+ clearTimeout(timeoutID);
+ reject(new Error("producer.network.request_timeout"));
+ });
+
+ producer.on("producer.disconnect", (_) => {
+ if (!connectedToKafka) {
+ clearTimeout(timeoutID);
+ reject(new Error("producer.disconnect"));
+ }
+ });
+ });
+};
+
/**
* Use NTLM Auth for a http request.
* @param {Object} options The http request options
diff --git a/src/assets/app.scss b/src/assets/app.scss
index b648cbcd..0eff9a06 100644
--- a/src/assets/app.scss
+++ b/src/assets/app.scss
@@ -436,12 +436,12 @@ optgroup {
.monitor-list {
&.scrollbar {
overflow-y: auto;
- height: calc(100% - 65px);
+ height: calc(100% - 107px);
}
@media (max-width: 770px) {
&.scrollbar {
- height: calc(100% - 40px);
+ height: calc(100% - 97px);
}
}
diff --git a/src/components/BadgeGeneratorDialog.vue b/src/components/BadgeGeneratorDialog.vue
index 9e073e39..aa6fa6e8 100644
--- a/src/components/BadgeGeneratorDialog.vue
+++ b/src/components/BadgeGeneratorDialog.vue
@@ -22,78 +22,78 @@
-
-
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -131,6 +135,7 @@
@@ -159,8 +205,6 @@ export default {
margin: -10px;
margin-bottom: 10px;
padding: 10px;
- display: flex;
- justify-content: space-between;
.dark & {
background-color: $dark-header-bg;
@@ -168,6 +212,17 @@ export default {
}
}
+.header-top {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.header-filter {
+ display: flex;
+ align-items: center;
+}
+
@media (max-width: 770px) {
.list-header {
margin: -20px;
@@ -216,5 +271,4 @@ export default {
padding-left: 67px;
margin-top: 5px;
}
-
diff --git a/src/components/MonitorListFilter.vue b/src/components/MonitorListFilter.vue
new file mode 100644
index 00000000..dbb1eb94
--- /dev/null
+++ b/src/components/MonitorListFilter.vue
@@ -0,0 +1,284 @@
+
+
+
+
+
+
+
+ {{ $t('Status') }}
+
+
+
+
+
+
+
+
+ {{ $root.stats.up }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $root.stats.down }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $root.stats.pending }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $root.stats.maintenance }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t("Running") }}
+ {{ $t("filterActivePaused") }}
+
+
+ {{ $t("filterActive") }}
+
+
+
+
+
+
+ {{ $t("Running") }}
+
+ {{ $root.stats.active }}
+
+
+
+
+
+
+
+
+
+
+ {{ $t("filterActivePaused") }}
+
+ {{ $root.stats.pause }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('Tags') }}
+
+
+
+
+
+
+
+
+ {{ getTaggedMonitorCount(tag) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/MonitorListFilterDropdown.vue b/src/components/MonitorListFilterDropdown.vue
new file mode 100644
index 00000000..01b9678f
--- /dev/null
+++ b/src/components/MonitorListFilterDropdown.vue
@@ -0,0 +1,131 @@
+
+
+
+
+
+
+
diff --git a/src/components/NotificationDialog.vue b/src/components/NotificationDialog.vue
index 856d6f53..f977225f 100644
--- a/src/components/NotificationDialog.vue
+++ b/src/components/NotificationDialog.vue
@@ -164,6 +164,7 @@ export default {
"SMSManager": "SmsManager (smsmanager.cz)",
"WeCom": "WeCom (企业微信群机器人)",
"ServerChan": "ServerChan (Server酱)",
+ "smsc": "SMSC",
};
// Sort by notification name
diff --git a/src/components/TagEditDialog.vue b/src/components/TagEditDialog.vue
index bdfbe132..e601aa42 100644
--- a/src/components/TagEditDialog.vue
+++ b/src/components/TagEditDialog.vue
@@ -99,7 +99,7 @@
+
+
+
+
+
+
+ {{ $t("aboutNotifyChannel") }}
+
diff --git a/src/components/notifications/index.js b/src/components/notifications/index.js
index 7b5e6b6c..673a84a9 100644
--- a/src/components/notifications/index.js
+++ b/src/components/notifications/index.js
@@ -4,6 +4,7 @@ import AliyunSMS from "./AliyunSms.vue";
import Apprise from "./Apprise.vue";
import Bark from "./Bark.vue";
import ClickSendSMS from "./ClickSendSMS.vue";
+import SMSC from "./SMSC.vue";
import DingDing from "./DingDing.vue";
import Discord from "./Discord.vue";
import Feishu from "./Feishu.vue";
@@ -61,6 +62,7 @@ const NotificationFormList = {
"apprise": Apprise,
"Bark": Bark,
"clicksendsms": ClickSendSMS,
+ "smsc": SMSC,
"DingDing": DingDing,
"discord": Discord,
"Feishu": Feishu,
diff --git a/src/lang/en.json b/src/lang/en.json
index 504e1a3c..5cdbc171 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -155,6 +155,8 @@
"Disable 2FA": "Disable 2FA",
"2FA Settings": "2FA Settings",
"Two Factor Authentication": "Two Factor Authentication",
+ "filterActive": "Active",
+ "filterActivePaused": "Paused",
"Active": "Active",
"Inactive": "Inactive",
"Token": "Token",
@@ -640,6 +642,8 @@
"matrixDesc1": "You can find the internal room ID by looking in the advanced section of the room settings in your Matrix client. It should look like !QMdRCpUIfLwsfjxye6:home.server.",
"matrixDesc2": "It is highly recommended you create a new user and do not use your own Matrix user's access token as it will allow full access to your account and all the rooms you joined. Instead, create a new user and only invite it to the room that you want to receive the notification in. You can get the access token by running {0}",
"Channel Name": "Channel Name",
+ "Notify Channel": "Notify Channel",
+ "aboutNotifyChannel": "Notify channel will trigger a desktop or mobile notification for all members of the channel, whether their availability is set to active or away.",
"Uptime Kuma URL": "Uptime Kuma URL",
"Icon Emoji": "Icon Emoji",
"signalImportant": "IMPORTANT: You cannot mix groups and numbers in recipients!",
@@ -743,13 +747,14 @@
"Open Badge Generator": "Open Badge Generator",
"Badge Generator": "{0}'s Badge Generator",
"Badge Type": "Badge Type",
- "Badge Duration": "Badge Duration",
+ "Badge Duration (in hours)": "Badge Duration (in hours)",
"Badge Label": "Badge Label",
- "Badge Prefix": "Badge Prefix",
- "Badge Suffix": "Badge Suffix",
+ "Badge Prefix": "Badge Value Prefix",
+ "Badge Suffix": "Badge Value Suffix",
"Badge Label Color": "Badge Label Color",
"Badge Color": "Badge Color",
"Badge Label Prefix": "Badge Label Prefix",
+ "Badge Preview": "Badge Preview",
"Badge Label Suffix": "Badge Label Suffix",
"Badge Up Color": "Badge Up Color",
"Badge Down Color": "Badge Down Color",
@@ -763,6 +768,20 @@
"Badge URL": "Badge URL",
"Group": "Group",
"Monitor Group": "Monitor Group",
+ "Kafka Brokers": "Kafka Brokers",
+ "Enter the list of brokers": "Enter the list of brokers",
+ "Press Enter to add broker": "Press Enter to add broker",
+ "Kafka Topic Name": "Kafka Topic Name",
+ "Kafka Producer Message": "Kafka Producer Message",
+ "Enable Kafka SSL": "Enable Kafka SSL",
+ "Enable Kafka Producer Auto Topic Creation": "Enable Kafka Producer Auto Topic Creation",
+ "Kafka SASL Options": "Kafka SASL Options",
+ "Mechanism": "Mechanism",
+ "Pick a SASL Mechanism...": "Pick a SASL Mechanism...",
+ "Authorization Identity": "Authorization Identity",
+ "AccessKey Id": "AccessKey Id",
+ "Secret AccessKey": "Secret AccessKey",
+ "Session Token": "Session Token",
"noGroupMonitorMsg": "Not Available. Create a Group Monitor First.",
"Close": "Close",
"Request Body": "Request Body",
diff --git a/src/lang/zh-HK.json b/src/lang/zh-HK.json
index fd5d35e3..aa43caa5 100644
--- a/src/lang/zh-HK.json
+++ b/src/lang/zh-HK.json
@@ -139,6 +139,8 @@
"Disable 2FA": "關閉 2FA",
"2FA Settings": "2FA 設定",
"Two Factor Authentication": "雙重認證",
+ "filterActive": "執行狀態",
+ "filterActivePaused": "已暫停",
"Active": "生效",
"Inactive": "未生效",
"Token": "Token",
diff --git a/src/mixins/public.js b/src/mixins/public.js
index a3e12f46..c87bfb35 100644
--- a/src/mixins/public.js
+++ b/src/mixins/public.js
@@ -1,9 +1,12 @@
import axios from "axios";
+import { getDevContainerServerHostname, isDevContainer } from "../util-frontend";
const env = process.env.NODE_ENV || "production";
// change the axios base url for development
-if (env === "development" || localStorage.dev === "dev") {
+if (env === "development" && isDevContainer()) {
+ axios.defaults.baseURL = location.protocol + "//" + getDevContainerServerHostname();
+} else if (env === "development" || localStorage.dev === "dev") {
axios.defaults.baseURL = location.protocol + "//" + location.hostname + ":3001";
}
diff --git a/src/mixins/socket.js b/src/mixins/socket.js
index e2834251..2d27d109 100644
--- a/src/mixins/socket.js
+++ b/src/mixins/socket.js
@@ -4,6 +4,7 @@ import jwtDecode from "jwt-decode";
import Favico from "favico.js";
import dayjs from "dayjs";
import { DOWN, MAINTENANCE, PENDING, UP } from "../util.ts";
+import { getDevContainerServerHostname, isDevContainer } from "../util-frontend.js";
const toast = useToast();
let socket;
@@ -93,7 +94,9 @@ export default {
let wsHost;
const env = process.env.NODE_ENV || "production";
- if (env === "development" || localStorage.dev === "dev") {
+ if (env === "development" && isDevContainer()) {
+ wsHost = protocol + getDevContainerServerHostname();
+ } else if (env === "development" || localStorage.dev === "dev") {
wsHost = protocol + location.hostname + ":3001";
} else {
wsHost = protocol + location.host;
@@ -693,9 +696,11 @@ export default {
stats() {
let result = {
+ active: 0,
up: 0,
down: 0,
maintenance: 0,
+ pending: 0,
unknown: 0,
pause: 0,
};
@@ -707,12 +712,13 @@ export default {
if (monitor && ! monitor.active) {
result.pause++;
} else if (beat) {
+ result.active++;
if (beat.status === UP) {
result.up++;
} else if (beat.status === DOWN) {
result.down++;
} else if (beat.status === PENDING) {
- result.up++;
+ result.pending++;
} else if (beat.status === MAINTENANCE) {
result.maintenance++;
} else {
diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue
index 1ce62279..0ffef8fe 100644
--- a/src/pages/EditMonitor.vue
+++ b/src/pages/EditMonitor.vue
@@ -61,6 +61,9 @@
+
@@ -166,6 +169,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -512,6 +566,56 @@
+
+
+
+ {{ $t("Kafka SASL Options") }}
+
+
+
+
+
+
+
{{ $t("HTTP Options") }}
@@ -724,6 +828,7 @@ export default {
},
acceptedStatusCodeOptions: [],
dnsresolvetypeOptions: [],
+ kafkaSaslMechanismOptions: [],
ipOrHostnameRegexPattern: hostNameRegexPattern(),
mqttIpOrHostnameRegexPattern: hostNameRegexPattern(true),
gameList: null,
@@ -987,12 +1092,21 @@ message HealthCheckResponse {
"TXT",
];
+ let kafkaSaslMechanismOptions = [
+ "None",
+ "plain",
+ "scram-sha-256",
+ "scram-sha-512",
+ "aws",
+ ];
+
for (let i = 100; i <= 999; i++) {
acceptedStatusCodeOptions.push(i.toString());
}
this.acceptedStatusCodeOptions = acceptedStatusCodeOptions;
this.dnsresolvetypeOptions = dnsresolvetypeOptions;
+ this.kafkaSaslMechanismOptions = kafkaSaslMechanismOptions;
},
methods: {
/** Initialize the edit monitor form */
@@ -1026,7 +1140,11 @@ message HealthCheckResponse {
mqttTopic: "",
mqttSuccessMessage: "",
authMethod: null,
- httpBodyEncoding: "json"
+ httpBodyEncoding: "json",
+ kafkaProducerBrokers: [],
+ kafkaProducerSaslOptions: {
+ mechanism: "None",
+ },
};
if (this.$root.proxyList && !this.monitor.proxyId) {
@@ -1067,6 +1185,7 @@ message HealthCheckResponse {
this.monitor.childrenIDs = undefined;
this.monitor.forceInactive = undefined;
this.monitor.pathName = undefined;
+ this.monitor.screenshot = undefined;
this.monitor.name = this.$t("cloneOf", [ this.monitor.name ]);
this.$refs.tagsManager.newTags = this.monitor.tags.map((monitorTag) => {
@@ -1093,6 +1212,10 @@ message HealthCheckResponse {
},
+ addKafkaProducerBroker(newBroker) {
+ this.monitor.kafkaProducerBrokers.push(newBroker);
+ },
+
/**
* Validate form input
* @returns {boolean} Is the form input valid?
diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue
index 5b777414..606d9657 100644
--- a/src/pages/StatusPage.vue
+++ b/src/pages/StatusPage.vue
@@ -331,7 +331,7 @@
-
{{ $t("Last Updated") }}:
+
{{ $t("Last Updated") }}: {{ lastUpdateTimeDisplay }}
{{ $tc("statusPageRefreshIn", [ updateCountdownText]) }}
@@ -366,7 +366,6 @@ import DOMPurify from "dompurify";
import Confirm from "../components/Confirm.vue";
import PublicGroupList from "../components/PublicGroupList.vue";
import MaintenanceTime from "../components/MaintenanceTime.vue";
-import DateTime from "../components/Datetime.vue";
import { getResBaseURL } from "../util-frontend";
import { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_MAINTENANCE, STATUS_PAGE_PARTIAL_DOWN, UP, MAINTENANCE } from "../util.ts";
import Tag from "../components/Tag.vue";
@@ -392,7 +391,6 @@ export default {
Confirm,
PrismEditor,
MaintenanceTime,
- DateTime,
Tag,
VueMultiselect
},
@@ -589,6 +587,10 @@ export default {
return "";
}
},
+
+ lastUpdateTimeDisplay() {
+ return this.$root.datetime(this.lastUpdateTime);
+ }
},
watch: {
diff --git a/src/util-frontend.js b/src/util-frontend.js
index 3a59dac5..4b85fa34 100644
--- a/src/util-frontend.js
+++ b/src/util-frontend.js
@@ -72,13 +72,32 @@ export function setPageLocale() {
*/
export function getResBaseURL() {
const env = process.env.NODE_ENV;
- if (env === "development" || localStorage.dev === "dev") {
+ if (env === "development" && isDevContainer()) {
+ return location.protocol + "//" + getDevContainerServerHostname();
+ } else if (env === "development" || localStorage.dev === "dev") {
return location.protocol + "//" + location.hostname + ":3001";
} else {
return "";
}
}
+export function isDevContainer() {
+ // eslint-disable-next-line no-undef
+ return (typeof DEVCONTAINER === "string" && DEVCONTAINER === "1");
+}
+
+/**
+ * Supports GitHub Codespaces only currently
+ */
+export function getDevContainerServerHostname() {
+ if (!isDevContainer()) {
+ return "";
+ }
+
+ // eslint-disable-next-line no-undef
+ return CODESPACE_NAME + "-3001." + GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN;
+}
+
/**
*
* @param {} mqtt wheather or not the regex should take into account the fact that it is an mqtt uri
diff --git a/test/backend.spec.js b/test/backend.spec.js
index 644a0fd0..0132fb77 100644
--- a/test/backend.spec.js
+++ b/test/backend.spec.js
@@ -306,6 +306,16 @@ describe("Test uptimeKumaServer.getClientIP()", () => {
ip = await server.getClientIP(fakeSocket);
expect(ip).toBe("203.0.113.195");
+ fakeSocket.client.conn.remoteAddress = "2001:db8::1";
+ fakeSocket.client.conn.request.headers = {};
+ ip = await server.getClientIP(fakeSocket);
+ expect(ip).toBe("2001:db8::1");
+
+ fakeSocket.client.conn.remoteAddress = "::ffff:127.0.0.1";
+ fakeSocket.client.conn.request.headers = {};
+ ip = await server.getClientIP(fakeSocket);
+ expect(ip).toBe("127.0.0.1");
+
await Database.close();
}, 120000);
});