From 9a3613856cc214468e424850abfccfbf9575d947 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abass=20=F0=9F=8D=89?= Date: Thu, 23 Oct 2025 22:59:32 +0300 Subject: [PATCH 01/43] Change Relative Time Formatter options to 'always' (#6240) --- src/util-frontend.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/util-frontend.js b/src/util-frontend.js index a3dc4c3ac..7ed3a0cf2 100644 --- a/src/util-frontend.js +++ b/src/util-frontend.js @@ -219,7 +219,7 @@ class RelativeTimeFormatter { * Default locale and options for Relative Time Formatter */ constructor() { - this.options = { numeric: "auto" }; + this.options = { numeric: "always" }; this.instance = new Intl.RelativeTimeFormat(currentLocale(), this.options); } @@ -267,7 +267,7 @@ class RelativeTimeFormatter { }; if (days > 0) { - toFormattedPart(days, "days"); + toFormattedPart(days, "day"); } if (hours > 0) { toFormattedPart(hours, "hour"); From cd49700d3fc9a508a1e72d457e011eca4c18bf84 Mon Sep 17 00:00:00 2001 From: Max Michels <6703026+maxmichels@users.noreply.github.com> Date: Fri, 24 Oct 2025 21:50:25 +0200 Subject: [PATCH 02/43] Adding retries to Google Chat Notifications #6242 (#6245) Co-authored-by: Frank Elsinga --- server/notification-providers/google-chat.js | 27 ++++++++++++++++++-- src/components/notifications/GoogleChat.vue | 14 ++++++++++ src/lang/en.json | 4 ++- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/server/notification-providers/google-chat.js b/server/notification-providers/google-chat.js index 1e2eb3507..2557a142c 100644 --- a/server/notification-providers/google-chat.js +++ b/server/notification-providers/google-chat.js @@ -12,6 +12,29 @@ class GoogleChat extends NotificationProvider { async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { const okMsg = "Sent Successfully."; + // If Google Chat Webhook rate limit is reached, retry to configured max retries defaults to 3, delay between 60-180 seconds + const post = async (url, data, config) => { + let retries = notification.googleChatMaxRetries || 1; // Default to 1 retries + retries = (retries > 10) ? 10 : retries; // Enforce maximum retries in backend + while (retries > 0) { + try { + await axios.post(url, data, config); + return; + } catch (error) { + if (error.response && error.response.status === 429) { + retries--; + if (retries === 0) { + throw error; + } + const delay = 60000 + Math.random() * 120000; + await new Promise(resolve => setTimeout(resolve, delay)); + } else { + throw error; + } + } + } + }; + try { let config = this.getAxiosConfigWithProxy({}); // Google Chat message formatting: https://developers.google.com/chat/api/guides/message-formats/basic @@ -24,7 +47,7 @@ class GoogleChat extends NotificationProvider { heartbeatJSON ); const data = { "text": renderedText }; - await axios.post(notification.googleChatWebhookURL, data, config); + await post(notification.googleChatWebhookURL, data, config); return okMsg; } @@ -96,7 +119,7 @@ class GoogleChat extends NotificationProvider { ], }; - await axios.post(notification.googleChatWebhookURL, data, config); + await post(notification.googleChatWebhookURL, data, config); return okMsg; } catch (error) { this.throwGeneralAxiosError(error); diff --git a/src/components/notifications/GoogleChat.vue b/src/components/notifications/GoogleChat.vue index 7b595ed59..90424cb2c 100644 --- a/src/components/notifications/GoogleChat.vue +++ b/src/components/notifications/GoogleChat.vue @@ -11,6 +11,14 @@ +
+ + +
+ {{ $t("Number of retry attempts if webhook fails") }} +
+
+
@@ -45,5 +53,11 @@ export default { ]); } }, + mounted() { + // Initialize default if needed + if (!this.$parent.notification.googleChatMaxRetries) { + this.$parent.notification.googleChatMaxRetries ||= 1; + } + }, }; diff --git a/src/lang/en.json b/src/lang/en.json index f3faaa8dc..190154331 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -1170,5 +1170,7 @@ "Bot secret": "Bot secret", "Send UP silently": "Send UP silently", "Send DOWN silently": "Send DOWN silently", - "Installing a Nextcloud Talk bot requires administrative access to the server.": "Installing a Nextcloud Talk bot requires administrative access to the server." + "Installing a Nextcloud Talk bot requires administrative access to the server.": "Installing a Nextcloud Talk bot requires administrative access to the server.", + "Number of retry attempts if webhook fails": "Number of retry attempts (every 60-180 seconds) if the webhook fails.", + "Maximum Retries": "Maximum Retries" } From 83c3cfc8c0f6bf6b04316cbe3fa1896268255694 Mon Sep 17 00:00:00 2001 From: Louis Lam Date: Sat, 25 Oct 2025 05:22:13 +0800 Subject: [PATCH 03/43] 2.0.X to master (#6226) --- docker/builder-go.dockerfile | 8 +++++++- package-lock.json | 4 ++-- package.json | 4 ++-- server/utils/simple-migration-server.js | 9 ++++++--- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/docker/builder-go.dockerfile b/docker/builder-go.dockerfile index 7c25641b4..3a9d78248 100644 --- a/docker/builder-go.dockerfile +++ b/docker/builder-go.dockerfile @@ -2,11 +2,17 @@ # Build in Golang # Run npm run build-healthcheck-armv7 in the host first, another it will be super slow where it is building the armv7 healthcheck ############################################ -FROM golang:1-bookworm +FROM golang:1-buster WORKDIR /app ARG TARGETPLATFORM COPY ./extra/ ./extra/ +## Switch to archive.debian.org +RUN sed -i '/^deb/s/^/#/' /etc/apt/sources.list \ + && echo "deb http://archive.debian.org/debian buster main contrib non-free" | tee -a /etc/apt/sources.list \ + && echo "deb http://archive.debian.org/debian-security buster/updates main contrib non-free" | tee -a /etc/apt/sources.list \ + && echo "deb http://archive.debian.org/debian buster-updates main contrib non-free" | tee -a /etc/apt/sources.list + # Compile healthcheck.go RUN apt update && \ apt --yes --no-install-recommends install curl && \ diff --git a/package-lock.json b/package-lock.json index dee7bc228..b063d8877 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "uptime-kuma", - "version": "2.0.0-beta.4", + "version": "2.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "uptime-kuma", - "version": "2.0.0-beta.4", + "version": "2.0.1", "license": "MIT", "dependencies": { "@grpc/grpc-js": "~1.8.22", diff --git a/package.json b/package.json index 541b71e11..576068492 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uptime-kuma", - "version": "2.0.1", + "version": "2.0.2", "license": "MIT", "repository": { "type": "git", @@ -41,7 +41,7 @@ "build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test2 --target pr-test2 . --push", "upload-artifacts": "node extra/release/upload-artifacts.mjs", "upload-artifacts-beta": "node extra/release/upload-artifacts-beta.mjs", - "setup": "git checkout 2.0.1 && npm ci --omit dev && npm run download-dist", + "setup": "git checkout 2.0.2 && npm ci --omit dev && npm run download-dist", "download-dist": "node extra/download-dist.js", "mark-as-nightly": "node extra/mark-as-nightly.js", "reset-password": "node extra/reset-password.js", diff --git a/server/utils/simple-migration-server.js b/server/utils/simple-migration-server.js index 895298560..1bc3b9475 100644 --- a/server/utils/simple-migration-server.js +++ b/server/utils/simple-migration-server.js @@ -39,11 +39,14 @@ class SimpleMigrationServer { this.app.get("/", (req, res) => { res.set("Content-Type", "text/html"); - // HTML meta tag redirect to /status + // Don't use meta tag redirect, it may cause issues in Chrome (#6223) res.end(` - - Migration server is running. + Uptime Kuma Migration + + Migration is in progress, it may take some time. You can check the progress in the console, or + click here to check. + `); }); From b7bb961eac18ac5a1c84e873d24247975f5e493d Mon Sep 17 00:00:00 2001 From: Paulus Lucas Date: Sun, 26 Oct 2025 10:10:16 +0100 Subject: [PATCH 04/43] Fix: release script do not update lock file correctly (#6257) --- extra/update-version.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/extra/update-version.js b/extra/update-version.js index 9e4593446..c32685741 100644 --- a/extra/update-version.js +++ b/extra/update-version.js @@ -27,7 +27,18 @@ if (! exists) { // Also update package-lock.json const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm"; - childProcess.spawnSync(npm, [ "install" ]); + const resultVersion = childProcess.spawnSync(npm, [ "--no-git-tag-version", "version", newVersion ], { shell: true }); + if (resultVersion.error) { + console.error(resultVersion.error); + console.error("error npm version!"); + process.exit(1); + } + const resultInstall = childProcess.spawnSync(npm, [ "install" ], { shell: true }); + if (resultInstall.error) { + console.error(resultInstall.error); + console.error("error update package-lock!"); + process.exit(1); + } commit(newVersion); } else { From 7bf25ba1bff80dac10169eb20c82c3a69f557c5c Mon Sep 17 00:00:00 2001 From: Tobi <84527857+reussio@users.noreply.github.com> Date: Sun, 26 Oct 2025 19:24:42 +0100 Subject: [PATCH 05/43] fix(auth/UX): trim username in login & setup (#6263) --- server/auth.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/auth.js b/server/auth.js index a4aed50b8..f98d4c8b3 100644 --- a/server/auth.js +++ b/server/auth.js @@ -18,8 +18,8 @@ exports.login = async function (username, password) { return null; } - let user = await R.findOne("user", " username = ? AND active = 1 ", [ - username, + let user = await R.findOne("user", "TRIM(username) = ? AND active = 1 ", [ + username.trim(), ]); if (user && passwordHash.verify(password, user.password)) { From c3a62f74613760b11330f25f2d570c5e0b443d18 Mon Sep 17 00:00:00 2001 From: Eric Duminil Date: Sun, 26 Oct 2025 20:36:47 +0100 Subject: [PATCH 06/43] Allow MQTT topic to have wildcards (# or +) (#5398) --- server/monitor-types/mqtt.js | 12 +++--- test/backend-test/test-mqtt.js | 67 +++++++++++++++++++++++++++++++--- 2 files changed, 67 insertions(+), 12 deletions(-) diff --git a/server/monitor-types/mqtt.js b/server/monitor-types/mqtt.js index 1865bbb42..18595b3a4 100644 --- a/server/monitor-types/mqtt.js +++ b/server/monitor-types/mqtt.js @@ -10,7 +10,7 @@ class MqttMonitorType extends MonitorType { * @inheritdoc */ async check(monitor, heartbeat, server) { - const receivedMessage = await this.mqttAsync(monitor.hostname, monitor.mqttTopic, { + const [ messageTopic, receivedMessage ] = await this.mqttAsync(monitor.hostname, monitor.mqttTopic, { port: monitor.port, username: monitor.mqttUsername, password: monitor.mqttPassword, @@ -25,7 +25,7 @@ class MqttMonitorType extends MonitorType { if (monitor.mqttCheckType === "keyword") { if (receivedMessage != null && receivedMessage.includes(monitor.mqttSuccessMessage)) { - heartbeat.msg = `Topic: ${monitor.mqttTopic}; Message: ${receivedMessage}`; + heartbeat.msg = `Topic: ${messageTopic}; Message: ${receivedMessage}`; heartbeat.status = UP; } else { throw Error(`Message Mismatch - Topic: ${monitor.mqttTopic}; Message: ${receivedMessage}`); @@ -110,11 +110,9 @@ class MqttMonitorType extends MonitorType { }); client.on("message", (messageTopic, message) => { - if (messageTopic === topic) { - client.end(); - clearTimeout(timeoutID); - resolve(message.toString("utf8")); - } + client.end(); + clearTimeout(timeoutID); + resolve([ messageTopic, message.toString("utf8") ]); }); }); diff --git a/test/backend-test/test-mqtt.js b/test/backend-test/test-mqtt.js index d616b12ed..921df48fc 100644 --- a/test/backend-test/test-mqtt.js +++ b/test/backend-test/test-mqtt.js @@ -10,16 +10,18 @@ const { UP, PENDING } = require("../../src/util"); * @param {string} mqttSuccessMessage the message that the monitor expects * @param {null|"keyword"|"json-query"} mqttCheckType the type of check we perform * @param {string} receivedMessage what message is received from the mqtt channel + * @param {string} monitorTopic which MQTT topic is monitored (wildcards are allowed) + * @param {string} publishTopic to which MQTT topic the message is sent * @returns {Promise} the heartbeat produced by the check */ -async function testMqtt(mqttSuccessMessage, mqttCheckType, receivedMessage) { +async function testMqtt(mqttSuccessMessage, mqttCheckType, receivedMessage, monitorTopic = "test", publishTopic = "test") { const hiveMQContainer = await new HiveMQContainer().start(); const connectionString = hiveMQContainer.getConnectionString(); const mqttMonitorType = new MqttMonitorType(); const monitor = { jsonPath: "firstProp", // always return firstProp for the json-query monitor hostname: connectionString.split(":", 2).join(":"), - mqttTopic: "test", + mqttTopic: monitorTopic, port: connectionString.split(":")[2], mqttUsername: null, mqttPassword: null, @@ -36,9 +38,9 @@ async function testMqtt(mqttSuccessMessage, mqttCheckType, receivedMessage) { const testMqttClient = mqtt.connect(hiveMQContainer.getConnectionString()); testMqttClient.on("connect", () => { - testMqttClient.subscribe("test", (error) => { + testMqttClient.subscribe(monitorTopic, (error) => { if (!error) { - testMqttClient.publish("test", receivedMessage); + testMqttClient.publish(publishTopic, receivedMessage); } }); }); @@ -53,7 +55,7 @@ async function testMqtt(mqttSuccessMessage, mqttCheckType, receivedMessage) { } describe("MqttMonitorType", { - concurrency: true, + concurrency: 4, skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64") }, () => { test("valid keywords (type=default)", async () => { @@ -62,11 +64,63 @@ describe("MqttMonitorType", { assert.strictEqual(heartbeat.msg, "Topic: test; Message: -> KEYWORD <-"); }); + test("valid nested topic", async () => { + const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-", "a/b/c", "a/b/c"); + assert.strictEqual(heartbeat.status, UP); + assert.strictEqual(heartbeat.msg, "Topic: a/b/c; Message: -> KEYWORD <-"); + }); + + test("valid nested topic (with special chars)", async () => { + const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-", "a/'/$/./*/%", "a/'/$/./*/%"); + assert.strictEqual(heartbeat.status, UP); + assert.strictEqual(heartbeat.msg, "Topic: a/'/$/./*/%; Message: -> KEYWORD <-"); + }); + + test("valid wildcard topic (with #)", async () => { + const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-", "a/#", "a/b/c"); + assert.strictEqual(heartbeat.status, UP); + assert.strictEqual(heartbeat.msg, "Topic: a/b/c; Message: -> KEYWORD <-"); + }); + + test("valid wildcard topic (with +)", async () => { + const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-", "a/+/c", "a/b/c"); + assert.strictEqual(heartbeat.status, UP); + assert.strictEqual(heartbeat.msg, "Topic: a/b/c; Message: -> KEYWORD <-"); + }); + + test("valid wildcard topic (with + and #)", async () => { + const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-", "a/+/c/#", "a/b/c/d/e"); + assert.strictEqual(heartbeat.status, UP); + assert.strictEqual(heartbeat.msg, "Topic: a/b/c/d/e; Message: -> KEYWORD <-"); + }); + + test("invalid topic", async () => { + await assert.rejects( + testMqtt("keyword will not be checked anyway", null, "message", "x/y/z", "a/b/c"), + new Error("Timeout, Message not received"), + ); + }); + + test("invalid wildcard topic (with #)", async () => { + await assert.rejects( + testMqtt("", null, "# should be last character", "#/c", "a/b/c"), + new Error("Timeout, Message not received"), + ); + }); + + test("invalid wildcard topic (with +)", async () => { + await assert.rejects( + testMqtt("", null, "message", "x/+/z", "a/b/c"), + new Error("Timeout, Message not received"), + ); + }); + test("valid keywords (type=keyword)", async () => { const heartbeat = await testMqtt("KEYWORD", "keyword", "-> KEYWORD <-"); assert.strictEqual(heartbeat.status, UP); assert.strictEqual(heartbeat.msg, "Topic: test; Message: -> KEYWORD <-"); }); + test("invalid keywords (type=default)", async () => { await assert.rejects( testMqtt("NOT_PRESENT", null, "-> KEYWORD <-"), @@ -80,12 +134,14 @@ describe("MqttMonitorType", { new Error("Message Mismatch - Topic: test; Message: -> KEYWORD <-"), ); }); + test("valid json-query", async () => { // works because the monitors' jsonPath is hard-coded to "firstProp" const heartbeat = await testMqtt("present", "json-query", "{\"firstProp\":\"present\"}"); assert.strictEqual(heartbeat.status, UP); assert.strictEqual(heartbeat.msg, "Message received, expected value is found"); }); + test("invalid (because query fails) json-query", async () => { // works because the monitors' jsonPath is hard-coded to "firstProp" await assert.rejects( @@ -93,6 +149,7 @@ describe("MqttMonitorType", { new Error("Message received but value is not equal to expected value, value was: [undefined]"), ); }); + test("invalid (because successMessage fails) json-query", async () => { // works because the monitors' jsonPath is hard-coded to "firstProp" await assert.rejects( From 93945606eaaf2d24e2c7abfef7f051a79de80eab Mon Sep 17 00:00:00 2001 From: Justin Keller Date: Mon, 27 Oct 2025 07:12:53 -0500 Subject: [PATCH 07/43] feat(release): reduce image size by running autoremove, clean and removing lists (#6267) --- docker/builder-go.dockerfile | 5 ++++- docker/debian-base.dockerfile | 24 +++++++++++++++--------- docker/dockerfile | 11 ++++++++--- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/docker/builder-go.dockerfile b/docker/builder-go.dockerfile index 3a9d78248..857476988 100644 --- a/docker/builder-go.dockerfile +++ b/docker/builder-go.dockerfile @@ -19,4 +19,7 @@ RUN apt update && \ curl -sL https://deb.nodesource.com/setup_18.x | bash && \ apt --yes --no-install-recommends install nodejs && \ node ./extra/build-healthcheck.js $TARGETPLATFORM && \ - apt --yes remove nodejs + apt --yes remove nodejs && \ + apt autoremove -y --purge && \ + apt clean && \ + rm -rf /var/lib/apt/lists/* diff --git a/docker/debian-base.dockerfile b/docker/debian-base.dockerfile index ca5fc43ec..14072ef5b 100644 --- a/docker/debian-base.dockerfile +++ b/docker/debian-base.dockerfile @@ -5,7 +5,10 @@ COPY ./extra/download-apprise.mjs ./download-apprise.mjs RUN apt update && \ apt --yes --no-install-recommends install curl && \ npm install cheerio semver && \ - node ./download-apprise.mjs + node ./download-apprise.mjs && \ + apt autoremove -y --purge && \ + apt clean && \ + rm -rf /var/lib/apt/lists/* # Base Image (Slim) # If the image changed, the second stage image should be changed too @@ -31,8 +34,9 @@ RUN apt update && \ curl \ sudo \ nscd && \ - rm -rf /var/lib/apt/lists/* && \ - apt --yes autoremove + apt autoremove -y --purge && \ + apt clean && \ + rm -rf /var/lib/apt/lists/* # apprise = for notifications (Install from the deb package, as the stable one is too old) (workaround for #4867) # Switching to testing repo is no longer working, as the testing repo is not bookworm anymore. @@ -41,9 +45,10 @@ RUN apt update && \ COPY --from=download-apprise /app/apprise.deb ./apprise.deb RUN apt update && \ apt --yes --no-install-recommends install ./apprise.deb python3-paho-mqtt && \ - rm -rf /var/lib/apt/lists/* && \ rm -f apprise.deb && \ - apt --yes autoremove + apt autoremove -y --purge && \ + apt clean && \ + rm -rf /var/lib/apt/lists/* # Install cloudflared RUN curl https://pkg.cloudflare.com/cloudflare-main.gpg --output /usr/share/keyrings/cloudflare-main.gpg && \ @@ -51,14 +56,14 @@ RUN curl https://pkg.cloudflare.com/cloudflare-main.gpg --output /usr/share/keyr apt update && \ apt install --yes --no-install-recommends cloudflared && \ cloudflared version && \ - rm -rf /var/lib/apt/lists/* && \ - apt --yes autoremove + apt autoremove -y --purge && \ + apt clean && \ + rm -rf /var/lib/apt/lists/* # For nscd COPY ./docker/etc/nscd.conf /etc/nscd.conf COPY ./docker/etc/sudoers /etc/sudoers - # Full Base Image # MariaDB, Chromium and fonts # Make sure to reuse the slim image here. Uncomment the above line if you want to build it from scratch. @@ -67,6 +72,7 @@ FROM louislam/uptime-kuma:base2-slim AS base2 ENV UPTIME_KUMA_ENABLE_EMBEDDED_MARIADB=1 RUN apt update && \ apt --yes --no-install-recommends install chromium fonts-indic fonts-noto fonts-noto-cjk mariadb-server && \ + apt autoremove -y --purge && \ + apt clean && \ rm -rf /var/lib/apt/lists/* && \ - apt --yes autoremove && \ chown -R node:node /var/lib/mysql diff --git a/docker/dockerfile b/docker/dockerfile index e2a301e7b..e19b8640e 100644 --- a/docker/dockerfile +++ b/docker/dockerfile @@ -70,7 +70,10 @@ RUN apt update \ && chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg \ && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ && apt update \ - && apt --yes --no-install-recommends install git + && apt --yes --no-install-recommends install git \ + && apt autoremove -y --purge \ + && apt clean \ + && rm -rf /var/lib/apt/lists/* ## Empty the directory, because we have to clone the Git repo. RUN rm -rf ./* && chown node /app @@ -95,7 +98,10 @@ CMD ["npm", "run", "start-pr-test"] FROM louislam/uptime-kuma:base2 AS upload-artifact WORKDIR / RUN apt update && \ - apt --yes install curl file + apt --yes install curl file && \ + apt autoremove -y --purge && \ + apt clean && \ + rm -rf /var/lib/apt/lists/* COPY --from=build /app /app @@ -115,4 +121,3 @@ RUN chmod +x /app/extra/upload-github-release-asset.sh # Dist only RUN cd /app && tar -zcvf $DIST dist RUN /app/extra/upload-github-release-asset.sh github_api_token=$GITHUB_TOKEN owner=louislam repo=uptime-kuma tag=$VERSION filename=/app/$DIST - From f6a47f351c45b440a61b5bba1305147e0b2a5f96 Mon Sep 17 00:00:00 2001 From: Louis Lam Date: Mon, 27 Oct 2025 22:10:29 +0800 Subject: [PATCH 08/43] Revert "feat(release): reduce image size by running autoremove, clean and removing lists" (#6268) --- docker/builder-go.dockerfile | 5 +---- docker/debian-base.dockerfile | 24 +++++++++--------------- docker/dockerfile | 11 +++-------- 3 files changed, 13 insertions(+), 27 deletions(-) diff --git a/docker/builder-go.dockerfile b/docker/builder-go.dockerfile index 857476988..3a9d78248 100644 --- a/docker/builder-go.dockerfile +++ b/docker/builder-go.dockerfile @@ -19,7 +19,4 @@ RUN apt update && \ curl -sL https://deb.nodesource.com/setup_18.x | bash && \ apt --yes --no-install-recommends install nodejs && \ node ./extra/build-healthcheck.js $TARGETPLATFORM && \ - apt --yes remove nodejs && \ - apt autoremove -y --purge && \ - apt clean && \ - rm -rf /var/lib/apt/lists/* + apt --yes remove nodejs diff --git a/docker/debian-base.dockerfile b/docker/debian-base.dockerfile index 14072ef5b..ca5fc43ec 100644 --- a/docker/debian-base.dockerfile +++ b/docker/debian-base.dockerfile @@ -5,10 +5,7 @@ COPY ./extra/download-apprise.mjs ./download-apprise.mjs RUN apt update && \ apt --yes --no-install-recommends install curl && \ npm install cheerio semver && \ - node ./download-apprise.mjs && \ - apt autoremove -y --purge && \ - apt clean && \ - rm -rf /var/lib/apt/lists/* + node ./download-apprise.mjs # Base Image (Slim) # If the image changed, the second stage image should be changed too @@ -34,9 +31,8 @@ RUN apt update && \ curl \ sudo \ nscd && \ - apt autoremove -y --purge && \ - apt clean && \ - rm -rf /var/lib/apt/lists/* + rm -rf /var/lib/apt/lists/* && \ + apt --yes autoremove # apprise = for notifications (Install from the deb package, as the stable one is too old) (workaround for #4867) # Switching to testing repo is no longer working, as the testing repo is not bookworm anymore. @@ -45,10 +41,9 @@ RUN apt update && \ COPY --from=download-apprise /app/apprise.deb ./apprise.deb RUN apt update && \ apt --yes --no-install-recommends install ./apprise.deb python3-paho-mqtt && \ + rm -rf /var/lib/apt/lists/* && \ rm -f apprise.deb && \ - apt autoremove -y --purge && \ - apt clean && \ - rm -rf /var/lib/apt/lists/* + apt --yes autoremove # Install cloudflared RUN curl https://pkg.cloudflare.com/cloudflare-main.gpg --output /usr/share/keyrings/cloudflare-main.gpg && \ @@ -56,14 +51,14 @@ RUN curl https://pkg.cloudflare.com/cloudflare-main.gpg --output /usr/share/keyr apt update && \ apt install --yes --no-install-recommends cloudflared && \ cloudflared version && \ - apt autoremove -y --purge && \ - apt clean && \ - rm -rf /var/lib/apt/lists/* + rm -rf /var/lib/apt/lists/* && \ + apt --yes autoremove # For nscd COPY ./docker/etc/nscd.conf /etc/nscd.conf COPY ./docker/etc/sudoers /etc/sudoers + # Full Base Image # MariaDB, Chromium and fonts # Make sure to reuse the slim image here. Uncomment the above line if you want to build it from scratch. @@ -72,7 +67,6 @@ FROM louislam/uptime-kuma:base2-slim AS base2 ENV UPTIME_KUMA_ENABLE_EMBEDDED_MARIADB=1 RUN apt update && \ apt --yes --no-install-recommends install chromium fonts-indic fonts-noto fonts-noto-cjk mariadb-server && \ - apt autoremove -y --purge && \ - apt clean && \ rm -rf /var/lib/apt/lists/* && \ + apt --yes autoremove && \ chown -R node:node /var/lib/mysql diff --git a/docker/dockerfile b/docker/dockerfile index e19b8640e..e2a301e7b 100644 --- a/docker/dockerfile +++ b/docker/dockerfile @@ -70,10 +70,7 @@ RUN apt update \ && chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg \ && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ && apt update \ - && apt --yes --no-install-recommends install git \ - && apt autoremove -y --purge \ - && apt clean \ - && rm -rf /var/lib/apt/lists/* + && apt --yes --no-install-recommends install git ## Empty the directory, because we have to clone the Git repo. RUN rm -rf ./* && chown node /app @@ -98,10 +95,7 @@ CMD ["npm", "run", "start-pr-test"] FROM louislam/uptime-kuma:base2 AS upload-artifact WORKDIR / RUN apt update && \ - apt --yes install curl file && \ - apt autoremove -y --purge && \ - apt clean && \ - rm -rf /var/lib/apt/lists/* + apt --yes install curl file COPY --from=build /app /app @@ -121,3 +115,4 @@ RUN chmod +x /app/extra/upload-github-release-asset.sh # Dist only RUN cd /app && tar -zcvf $DIST dist RUN /app/extra/upload-github-release-asset.sh github_api_token=$GITHUB_TOKEN owner=louislam repo=uptime-kuma tag=$VERSION filename=/app/$DIST + From 8f3cb770ebfeb812352839b4856f41d962e0fd86 Mon Sep 17 00:00:00 2001 From: Louis Lam Date: Mon, 27 Oct 2025 23:58:27 +0800 Subject: [PATCH 09/43] [Docker] Bump to Node.js 22 (#6222) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/auto-test.yml | 12 ++++++------ .github/workflows/close-incorrect-issue.yml | 4 ++-- .github/workflows/validate.yml | 2 +- docker/debian-base.dockerfile | 4 ++-- package.json | 6 ++++-- test/test-backend.mjs | 12 ++++++++++++ 6 files changed, 27 insertions(+), 13 deletions(-) create mode 100644 test/test-backend.mjs diff --git a/.github/workflows/auto-test.yml b/.github/workflows/auto-test.yml index f59035442..14b80f234 100644 --- a/.github/workflows/auto-test.yml +++ b/.github/workflows/auto-test.yml @@ -22,7 +22,7 @@ jobs: strategy: matrix: os: [macos-latest, ubuntu-22.04, windows-latest, ARM64] - node: [ 18, 20 ] + node: [ 20, 24 ] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: @@ -30,7 +30,7 @@ jobs: - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node }} - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: ${{ matrix.node }} - run: npm install @@ -49,7 +49,7 @@ jobs: strategy: matrix: os: [ ARMv7 ] - node: [ 18, 20 ] + node: [ 20, 22 ] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: @@ -57,7 +57,7 @@ jobs: - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node }} - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: ${{ matrix.node }} - run: npm ci --production @@ -70,7 +70,7 @@ jobs: - uses: actions/checkout@v4 - name: Use Node.js 20 - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: 20 - run: npm install @@ -84,7 +84,7 @@ jobs: - uses: actions/checkout@v4 - name: Use Node.js 20 - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: 20 - run: npm install diff --git a/.github/workflows/close-incorrect-issue.yml b/.github/workflows/close-incorrect-issue.yml index 3ef5ba378..9d4616931 100644 --- a/.github/workflows/close-incorrect-issue.yml +++ b/.github/workflows/close-incorrect-issue.yml @@ -11,13 +11,13 @@ jobs: strategy: matrix: os: [ubuntu-latest] - node-version: [18] + node-version: [20] steps: - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} cache: 'npm' diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 7e631ccd4..4dff3689d 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -32,7 +32,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Use Node.js 20 - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: 20 diff --git a/docker/debian-base.dockerfile b/docker/debian-base.dockerfile index ca5fc43ec..10471af2a 100644 --- a/docker/debian-base.dockerfile +++ b/docker/debian-base.dockerfile @@ -1,5 +1,5 @@ # Download Apprise deb package -FROM node:20-bookworm-slim AS download-apprise +FROM node:22-bookworm-slim AS download-apprise WORKDIR /app COPY ./extra/download-apprise.mjs ./download-apprise.mjs RUN apt update && \ @@ -9,7 +9,7 @@ RUN apt update && \ # Base Image (Slim) # If the image changed, the second stage image should be changed too -FROM node:20-bookworm-slim AS base2-slim +FROM node:22-bookworm-slim AS base2-slim ARG TARGETPLATFORM # Specify --no-install-recommends to skip unused dependencies, make the base much smaller! diff --git a/package.json b/package.json index 576068492..7f09ef86f 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://github.com/louislam/uptime-kuma.git" }, "engines": { - "node": "18 || >= 20.4.0" + "node": ">= 20.4.0" }, "scripts": { "lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .", @@ -27,7 +27,9 @@ "build": "vite build --config ./config/vite.config.js", "test": "npm run test-backend && npm run test-e2e", "test-with-build": "npm run build && npm test", - "test-backend": "cross-env TEST_BACKEND=1 node --test test/backend-test", + "test-backend": "node test/test-backend.mjs", + "test-backend-22": "cross-env TEST_BACKEND=1 node --test \"test/backend-test/**/*.js\"", + "test-backend-20": "cross-env TEST_BACKEND=1 node --test test/backend-test", "test-e2e": "playwright test --config ./config/playwright.config.js", "test-e2e-ui": "playwright test --config ./config/playwright.config.js --ui --ui-port=51063", "playwright-codegen": "playwright codegen localhost:3000 --save-storage=./private/e2e-auth.json", diff --git a/test/test-backend.mjs b/test/test-backend.mjs new file mode 100644 index 000000000..e285f7804 --- /dev/null +++ b/test/test-backend.mjs @@ -0,0 +1,12 @@ +import * as childProcess from "child_process"; + +const version = parseInt(process.version.slice(1).split(".")[0]); + +/** + * Since Node.js 22 introduced a different "node --test" command with glob, we need to run different test commands based on the Node.js version. + */ +if (version < 22) { + childProcess.execSync("npm run test-backend-20", { stdio: "inherit" }); +} else { + childProcess.execSync("npm run test-backend-22", { stdio: "inherit" }); +} From afbd1ce0e9923114cebae080647553865430bf89 Mon Sep 17 00:00:00 2001 From: Louis Lam Date: Tue, 28 Oct 2025 00:27:29 +0800 Subject: [PATCH 10/43] [Eliminate Blocking] Real Browser Monitor + Check Apprise (#5924) --- .../monitor-types/real-browser-monitor-type.js | 18 +++++++++--------- server/notification.js | 9 ++++----- server/server.js | 2 +- server/util-server.js | 16 ++++++++++++++++ 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/server/monitor-types/real-browser-monitor-type.js b/server/monitor-types/real-browser-monitor-type.js index 2a2871d2c..07b6c8aac 100644 --- a/server/monitor-types/real-browser-monitor-type.js +++ b/server/monitor-types/real-browser-monitor-type.js @@ -2,13 +2,13 @@ const { MonitorType } = require("./monitor-type"); const { chromium } = require("playwright-core"); const { UP, log } = require("../../src/util"); const { Settings } = require("../settings"); -const commandExistsSync = require("command-exists").sync; const childProcess = require("child_process"); const path = require("path"); const Database = require("../database"); const jwt = require("jsonwebtoken"); const config = require("../config"); const { RemoteBrowser } = require("../remote-browser"); +const { commandExists } = require("../util-server"); /** * Cached instance of a browser @@ -122,7 +122,7 @@ async function prepareChromeExecutable(executablePath) { executablePath = "/usr/bin/chromium"; // Install chromium in container via apt install - if ( !commandExistsSync(executablePath)) { + if (! await commandExists(executablePath)) { await new Promise((resolve, reject) => { log.info("Chromium", "Installing Chromium..."); let child = childProcess.exec("apt update && apt --yes --no-install-recommends install chromium fonts-indic fonts-noto fonts-noto-cjk"); @@ -146,7 +146,7 @@ async function prepareChromeExecutable(executablePath) { } } else { - executablePath = findChrome(allowedList); + executablePath = await findChrome(allowedList); } } else { // User specified a path @@ -160,20 +160,20 @@ async function prepareChromeExecutable(executablePath) { /** * Find the chrome executable - * @param {any[]} executables Executables to search through - * @returns {any} Executable - * @throws Could not find executable + * @param {string[]} executables Executables to search through + * @returns {Promise} Executable + * @throws {Error} Could not find executable */ -function findChrome(executables) { +async function findChrome(executables) { // Use the last working executable, so we don't have to search for it again if (lastAutoDetectChromeExecutable) { - if (commandExistsSync(lastAutoDetectChromeExecutable)) { + if (await commandExists(lastAutoDetectChromeExecutable)) { return lastAutoDetectChromeExecutable; } } for (let executable of executables) { - if (commandExistsSync(executable)) { + if (await commandExists(executable)) { lastAutoDetectChromeExecutable = executable; return executable; } diff --git a/server/notification.js b/server/notification.js index 8ad62dc13..31028e3dd 100644 --- a/server/notification.js +++ b/server/notification.js @@ -81,6 +81,7 @@ const Brevo = require("./notification-providers/brevo"); const YZJ = require("./notification-providers/yzj"); const SMSPlanet = require("./notification-providers/sms-planet"); const SpugPush = require("./notification-providers/spugpush"); +const { commandExists } = require("./util-server"); class Notification { providerList = {}; @@ -275,12 +276,10 @@ class Notification { /** * Check if apprise exists - * @returns {boolean} Does the command apprise exist? + * @returns {Promise} Does the command apprise exist? */ - static checkApprise() { - let commandExistsSync = require("command-exists").sync; - let exists = commandExistsSync("apprise"); - return exists; + static async checkApprise() { + return await commandExists("apprise"); } } diff --git a/server/server.js b/server/server.js index 55289b55a..6dba70a3e 100644 --- a/server/server.js +++ b/server/server.js @@ -1503,7 +1503,7 @@ let needSetup = false; socket.on("checkApprise", async (callback) => { try { checkLogin(socket); - callback(Notification.checkApprise()); + callback(await Notification.checkApprise()); } catch (e) { callback(false); } diff --git a/server/util-server.js b/server/util-server.js index ecad276b3..462b80578 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -1116,3 +1116,19 @@ function fsExists(path) { }); } module.exports.fsExists = fsExists; + +/** + * By default, command-exists will throw a null error if the command does not exist, which is ugly. The function makes it better. + * Read more: https://github.com/mathisonian/command-exists/issues/22 + * @param {string} command Command to check + * @returns {Promise} True if command exists, false otherwise + */ +async function commandExists(command) { + try { + await require("command-exists")(command); + return true; + } catch (e) { + return false; + } +} +module.exports.commandExists = commandExists; From a3672a6afb9fce3f618c21a07e552de44e863621 Mon Sep 17 00:00:00 2001 From: Louis Lam Date: Tue, 28 Oct 2025 04:01:24 +0800 Subject: [PATCH 11/43] Fix: disable eqeqeq for UPTIME_KUMA_LOG_RESPONSE_BODY_MONITOR_ID (#6271) --- server/model/monitor.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/model/monitor.js b/server/model/monitor.js index 178d639cd..6e2b3c033 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -578,7 +578,8 @@ class Monitor extends BeanModel { } } - if (process.env.UPTIME_KUMA_LOG_RESPONSE_BODY_MONITOR_ID === this.id) { + // eslint-disable-next-line eqeqeq + if (process.env.UPTIME_KUMA_LOG_RESPONSE_BODY_MONITOR_ID == this.id) { log.info("monitor", res.data); } From ea3a4f6963039cb5d35867b706a5d04e34c08554 Mon Sep 17 00:00:00 2001 From: Ashutosh Mohan Date: Tue, 28 Oct 2025 02:28:13 +0530 Subject: [PATCH 12/43] feat(status-page): add help text for 'Description' in monitor edit status page (#6254) Co-authored-by: Ashutosh Mohan Co-authored-by: Frank Elsinga --- src/lang/en.json | 1 + src/pages/EditMonitor.vue | 1 + 2 files changed, 2 insertions(+) diff --git a/src/lang/en.json b/src/lang/en.json index 190154331..43b12375f 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -306,6 +306,7 @@ "Show Tags": "Show Tags", "Hide Tags": "Hide Tags", "Description": "Description", + "descriptionHelpText": "Shown on the internal dashboard. Markdown is allowed and sanitized (preserves spaces and indentation) before display.", "No monitors available.": "No monitors available.", "Add one": "Add one", "No Monitors": "No Monitors", diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index f69b1c633..425cf4ff5 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -819,6 +819,7 @@
+
{{ $t("descriptionHelpText") }}
From 38ec3bc43295435052fec54504f16505c5c0ea2b Mon Sep 17 00:00:00 2001 From: maldotcom2 <149653530+maldotcom2@users.noreply.github.com> Date: Tue, 28 Oct 2025 08:09:21 +1100 Subject: [PATCH 13/43] Fix do nothing erroneous api call for Pagerduty (#6231) Co-authored-by: Frank Elsinga --- server/notification-providers/pagerduty.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/server/notification-providers/pagerduty.js b/server/notification-providers/pagerduty.js index c60d782e7..385ad2af0 100644 --- a/server/notification-providers/pagerduty.js +++ b/server/notification-providers/pagerduty.js @@ -23,9 +23,7 @@ class PagerDuty extends NotificationProvider { if (heartbeatJSON.status === UP) { const title = "Uptime Kuma Monitor ✅ Up"; - const eventAction = notification.pagerdutyAutoResolve || null; - - return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, eventAction); + return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, "resolve"); } if (heartbeatJSON.status === DOWN) { @@ -63,10 +61,6 @@ class PagerDuty extends NotificationProvider { */ async postNotification(notification, title, body, monitorInfo, eventAction = "trigger") { - if (eventAction == null) { - return "No action required"; - } - let monitorUrl; if (monitorInfo.type === "port") { monitorUrl = monitorInfo.hostname; @@ -79,6 +73,13 @@ class PagerDuty extends NotificationProvider { monitorUrl = monitorInfo.url; } + if (eventAction === "resolve") { + if (notification.pagerdutyAutoResolve === "0") { + return "no action required"; + } + eventAction = notification.pagerdutyAutoResolve; + } + const options = { method: "POST", url: notification.pagerdutyIntegrationUrl, From 19c2bbd586269ca752e663783dcb64d1b420a885 Mon Sep 17 00:00:00 2001 From: aruj0 <136828596+aruj0@users.noreply.github.com> Date: Mon, 27 Oct 2025 21:19:05 +0000 Subject: [PATCH 14/43] Feature/webhook get method support (#6194) Co-authored-by: Frank Elsinga --- server/notification-providers/webhook.js | 24 ++++++++++++++++++++++-- src/components/notifications/Webhook.vue | 20 ++++++++++++++++++++ src/lang/en.json | 3 +++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/server/notification-providers/webhook.js b/server/notification-providers/webhook.js index 77ce229d8..ab7d5f069 100644 --- a/server/notification-providers/webhook.js +++ b/server/notification-providers/webhook.js @@ -12,6 +12,8 @@ class Webhook extends NotificationProvider { const okMsg = "Sent Successfully."; try { + const httpMethod = notification.httpMethod.toLowerCase() || "post"; + let data = { heartbeat: heartbeatJSON, monitor: monitorJSON, @@ -21,7 +23,19 @@ class Webhook extends NotificationProvider { headers: {} }; - if (notification.webhookContentType === "form-data") { + if (httpMethod === "get") { + config.params = { + msg: msg + }; + + if (heartbeatJSON) { + config.params.heartbeat = JSON.stringify(heartbeatJSON); + } + + if (monitorJSON) { + config.params.monitor = JSON.stringify(monitorJSON); + } + } else if (notification.webhookContentType === "form-data") { const formData = new FormData(); formData.append("data", JSON.stringify(data)); config.headers = formData.getHeaders(); @@ -42,7 +56,13 @@ class Webhook extends NotificationProvider { } config = this.getAxiosConfigWithProxy(config); - await axios.post(notification.webhookURL, data, config); + + if (httpMethod === "get") { + await axios.get(notification.webhookURL, config); + } else { + await axios.post(notification.webhookURL, data, config); + } + return okMsg; } catch (error) { diff --git a/src/components/notifications/Webhook.vue b/src/components/notifications/Webhook.vue index 7775a3fdd..be51cc2c6 100644 --- a/src/components/notifications/Webhook.vue +++ b/src/components/notifications/Webhook.vue @@ -12,6 +12,21 @@
+ + +
+ {{ $parent.notification.httpMethod === 'get' ? $t("webhookGetMethodDesc") : $t("webhookPostMethodDesc") }} +
+
+ +