From b22969a3e6c6fdf226325eca08347f369c0c4512 Mon Sep 17 00:00:00 2001
From: youpie <37704067+youpie@users.noreply.github.com>
Date: Thu, 1 May 2025 19:36:34 +0200
Subject: [PATCH 1/8] Allow HTML for custom email bodies (#5635)
---
server/notification-providers/smtp.js | 6 ++++--
src/components/notifications/SMTP.vue | 9 +++++++++
src/lang/en.json | 1 +
3 files changed, 14 insertions(+), 2 deletions(-)
diff --git a/server/notification-providers/smtp.js b/server/notification-providers/smtp.js
index 980c7dfd3..d0ad5b728 100644
--- a/server/notification-providers/smtp.js
+++ b/server/notification-providers/smtp.js
@@ -42,6 +42,7 @@ class SMTP extends NotificationProvider {
// default values in case the user does not want to template
let subject = msg;
let body = msg;
+ let useHTMLBody = false;
if (heartbeatJSON) {
body = `${msg}\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`;
}
@@ -50,11 +51,11 @@ class SMTP extends NotificationProvider {
// cannot end with whitespace as this often raises spam scores
const customSubject = notification.customSubject?.trim() || "";
const customBody = notification.customBody?.trim() || "";
-
if (customSubject !== "") {
subject = await this.renderTemplate(customSubject, msg, monitorJSON, heartbeatJSON);
}
if (customBody !== "") {
+ useHTMLBody = notification.htmlBody || false;
body = await this.renderTemplate(customBody, msg, monitorJSON, heartbeatJSON);
}
}
@@ -67,7 +68,8 @@ class SMTP extends NotificationProvider {
bcc: notification.smtpBCC,
to: notification.smtpTo,
subject: subject,
- text: body,
+ // If the email body is custom, and the user wants it, set the email body as HTML
+ [useHTMLBody ? "html" : "text"]: body
});
return okMsg;
diff --git a/src/components/notifications/SMTP.vue b/src/components/notifications/SMTP.vue
index 4e0fb4b57..8f0e1501d 100644
--- a/src/components/notifications/SMTP.vue
+++ b/src/components/notifications/SMTP.vue
@@ -79,6 +79,15 @@
{{ $t("leave blank for default body") }}
+
+
+
+
+
+
+
{{ $t("documentation") }}
diff --git a/src/lang/en.json b/src/lang/en.json
index b089478f8..63c28b56c 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -517,6 +517,7 @@
"Clone": "Clone",
"cloneOf": "Clone of {0}",
"smtp": "Email (SMTP)",
+ "Use HTML for custom E-mail body": "Use HTML for custom E-mail body",
"secureOptionNone": "None / STARTTLS (25, 587)",
"secureOptionTLS": "TLS (465)",
"Ignore TLS Error": "Ignore TLS Error",
From 6a5011ad34ee7c1507cada9b9fced59a4b7245b6 Mon Sep 17 00:00:00 2001
From: vapao
Date: Fri, 2 May 2025 14:58:16 +0800
Subject: [PATCH 2/8] Add SpugPush notification provider (#5813)
Co-authored-by: Frank Elsinga
---
server/notification-providers/spugpush.js | 37 +++++++++++++++++++++++
server/notification.js | 2 ++
src/components/NotificationDialog.vue | 1 +
src/components/notifications/SpugPush.vue | 19 ++++++++++++
src/components/notifications/index.js | 2 ++
src/lang/en.json | 1 +
6 files changed, 62 insertions(+)
create mode 100644 server/notification-providers/spugpush.js
create mode 100644 src/components/notifications/SpugPush.vue
diff --git a/server/notification-providers/spugpush.js b/server/notification-providers/spugpush.js
new file mode 100644
index 000000000..e84174f38
--- /dev/null
+++ b/server/notification-providers/spugpush.js
@@ -0,0 +1,37 @@
+const NotificationProvider = require("./notification-provider");
+const axios = require("axios");
+const { DOWN, UP } = require("../../src/util");
+
+class SpugPush extends NotificationProvider {
+
+ name = "SpugPush";
+
+ /**
+ * @inheritdoc
+ */
+ async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
+ let okMsg = "Sent Successfully.";
+ try {
+ let formData = {
+ msg
+ };
+ const apiUrl = `https://push.spug.cc/send/${notification.templateKey}`;
+ if (heartbeatJSON == null) {
+ formData.title = "Uptime Kuma Message";
+ } else if (heartbeatJSON["status"] === UP) {
+ formData.title = `UptimeKuma 「${monitorJSON["name"]}」 is Up`;
+ formData.msg = `[✅ Up] ${heartbeatJSON["msg"]}`;
+ } else if (heartbeatJSON["status"] === DOWN) {
+ formData.title = `UptimeKuma 「${monitorJSON["name"]}」 is Down`;
+ formData.msg = `[🔴 Down] ${heartbeatJSON["msg"]}`;
+ }
+
+ await axios.post(apiUrl, formData);
+ return okMsg;
+ } catch (error) {
+ this.throwGeneralAxiosError(error);
+ }
+ }
+}
+
+module.exports = SpugPush;
diff --git a/server/notification.js b/server/notification.js
index b3f745854..468d026c0 100644
--- a/server/notification.js
+++ b/server/notification.js
@@ -75,6 +75,7 @@ const Wpush = require("./notification-providers/wpush");
const SendGrid = require("./notification-providers/send-grid");
const YZJ = require("./notification-providers/yzj");
const SMSPlanet = require("./notification-providers/sms-planet");
+const SpugPush = require("./notification-providers/spugpush");
class Notification {
@@ -167,6 +168,7 @@ class Notification {
new SendGrid(),
new YZJ(),
new SMSPlanet(),
+ new SpugPush(),
];
for (let item of list) {
if (! item.name) {
diff --git a/src/components/NotificationDialog.vue b/src/components/NotificationDialog.vue
index 1b15616aa..2e66de8e9 100644
--- a/src/components/NotificationDialog.vue
+++ b/src/components/NotificationDialog.vue
@@ -185,6 +185,7 @@ export default {
"WeCom": "WeCom (企业微信群机器人)",
"ServerChan": "ServerChan (Server酱)",
"PushPlus": "PushPlus (推送加)",
+ "SpugPush": "SpugPush(Spug推送助手)",
"smsc": "SMSC",
"WPush": "WPush(wpush.cn)",
"YZJ": "YZJ (云之家自定义机器人)",
diff --git a/src/components/notifications/SpugPush.vue b/src/components/notifications/SpugPush.vue
new file mode 100644
index 000000000..37dffccf3
--- /dev/null
+++ b/src/components/notifications/SpugPush.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ https://push.spug.cc
+
+
+
+
diff --git a/src/components/notifications/index.js b/src/components/notifications/index.js
index 70e7d336d..be7feb820 100644
--- a/src/components/notifications/index.js
+++ b/src/components/notifications/index.js
@@ -64,6 +64,7 @@ import WeCom from "./WeCom.vue";
import GoAlert from "./GoAlert.vue";
import ZohoCliq from "./ZohoCliq.vue";
import Splunk from "./Splunk.vue";
+import SpugPush from "./SpugPush.vue";
import SevenIO from "./SevenIO.vue";
import Whapi from "./Whapi.vue";
import WAHA from "./WAHA.vue";
@@ -140,6 +141,7 @@ const NotificationFormList = {
"threema": Threema,
"twilio": Twilio,
"Splunk": Splunk,
+ "SpugPush": SpugPush,
"webhook": Webhook,
"WeCom": WeCom,
"GoAlert": GoAlert,
diff --git a/src/lang/en.json b/src/lang/en.json
index 63c28b56c..37c023ee4 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -799,6 +799,7 @@
"PushDeer Server": "PushDeer Server",
"pushDeerServerDescription": "Leave blank to use the official server",
"PushDeer Key": "PushDeer Key",
+ "SpugPush Template Code": "Template Code",
"wayToGetClickSendSMSToken": "You can get API Username and API Key from {0} .",
"Custom Monitor Type": "Custom Monitor Type",
"Google Analytics ID": "Google Analytics ID",
From 32d92eccfae0280c8566566ec3c623dad41fed8e Mon Sep 17 00:00:00 2001
From: vapao
Date: Sun, 4 May 2025 20:42:40 +0800
Subject: [PATCH 3/8] Update SpugPush notification payload (#5816)
---
server/notification-providers/spugpush.js | 22 +++++++++++-----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/server/notification-providers/spugpush.js b/server/notification-providers/spugpush.js
index e84174f38..b04a20cac 100644
--- a/server/notification-providers/spugpush.js
+++ b/server/notification-providers/spugpush.js
@@ -13,19 +13,19 @@ class SpugPush extends NotificationProvider {
let okMsg = "Sent Successfully.";
try {
let formData = {
- msg
+ title: "Uptime Kuma Message",
+ content: msg
};
- const apiUrl = `https://push.spug.cc/send/${notification.templateKey}`;
- if (heartbeatJSON == null) {
- formData.title = "Uptime Kuma Message";
- } else if (heartbeatJSON["status"] === UP) {
- formData.title = `UptimeKuma 「${monitorJSON["name"]}」 is Up`;
- formData.msg = `[✅ Up] ${heartbeatJSON["msg"]}`;
- } else if (heartbeatJSON["status"] === DOWN) {
- formData.title = `UptimeKuma 「${monitorJSON["name"]}」 is Down`;
- formData.msg = `[🔴 Down] ${heartbeatJSON["msg"]}`;
+ if (heartbeatJSON) {
+ if (heartbeatJSON["status"] === UP) {
+ formData.title = `UptimeKuma 「${monitorJSON["name"]}」 is Up`;
+ formData.content = `[✅ Up] ${heartbeatJSON["msg"]}`;
+ } else if (heartbeatJSON["status"] === DOWN) {
+ formData.title = `UptimeKuma 「${monitorJSON["name"]}」 is Down`;
+ formData.content = `[🔴 Down] ${heartbeatJSON["msg"]}`;
+ }
}
-
+ const apiUrl = `https://push.spug.cc/send/${notification.templateKey}`;
await axios.post(apiUrl, formData);
return okMsg;
} catch (error) {
From 76c382f229119adf100e28b1d137e203dedfa38d Mon Sep 17 00:00:00 2001
From: Cyril59310 <70776486+cyril59310@users.noreply.github.com>
Date: Tue, 6 May 2025 01:33:49 +0200
Subject: [PATCH 4/8] Add disable url option in notification Discord (#5817)
Co-authored-by: Frank Elsinga
---
server/notification-providers/discord.js | 8 ++++----
src/components/notifications/Discord.vue | 10 ++++++++++
src/lang/en.json | 3 ++-
3 files changed, 16 insertions(+), 5 deletions(-)
diff --git a/server/notification-providers/discord.js b/server/notification-providers/discord.js
index 6a52f8f3e..9ea260410 100644
--- a/server/notification-providers/discord.js
+++ b/server/notification-providers/discord.js
@@ -46,10 +46,10 @@ class Discord extends NotificationProvider {
name: "Service Name",
value: monitorJSON["name"],
},
- {
+ ...(!notification.disableUrl ? [{
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
value: this.extractAddress(monitorJSON),
- },
+ }] : []),
{
name: `Time (${heartbeatJSON["timezone"]})`,
value: heartbeatJSON["localDateTime"],
@@ -83,10 +83,10 @@ class Discord extends NotificationProvider {
name: "Service Name",
value: monitorJSON["name"],
},
- {
+ ...(!notification.disableUrl ? [{
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
value: this.extractAddress(monitorJSON),
- },
+ }] : []),
{
name: `Time (${heartbeatJSON["timezone"]})`,
value: heartbeatJSON["localDateTime"],
diff --git a/src/components/notifications/Discord.vue b/src/components/notifications/Discord.vue
index 5d8334f5f..40d2f204e 100644
--- a/src/components/notifications/Discord.vue
+++ b/src/components/notifications/Discord.vue
@@ -53,6 +53,13 @@
+
+
+
+
+
+
+
diff --git a/src/lang/en.json b/src/lang/en.json
index 37c023ee4..009d24ee6 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -1093,5 +1093,6 @@
"the smsplanet documentation": "the smsplanet documentation",
"Phone numbers": "Phone numbers",
"Sender name": "Sender name",
- "smsplanetNeedToApproveName": "Needs to be approved in the client panel"
+ "smsplanetNeedToApproveName": "Needs to be approved in the client panel",
+ "Disable URL in Notification": "Disable URL in Notification"
}
From eb18677e4fa00f9c709451d8f37e82858419a119 Mon Sep 17 00:00:00 2001
From: Pargorn Ruasijan
Date: Thu, 8 May 2025 03:29:27 +0700
Subject: [PATCH 5/8] fixed: #5564 slack notifications no preview available
(#5824)
---
server/notification-providers/slack.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/server/notification-providers/slack.js b/server/notification-providers/slack.js
index 5e25a1fbc..455d787c7 100644
--- a/server/notification-providers/slack.js
+++ b/server/notification-providers/slack.js
@@ -145,6 +145,7 @@ class Slack extends NotificationProvider {
const title = "Uptime Kuma Alert";
let data = {
+ "text": msg,
"channel": notification.slackchannel,
"username": notification.slackusername,
"icon_emoji": notification.slackiconemo,
From 86b3ef9c864bb66f48fc2b87a03c675362d8081c Mon Sep 17 00:00:00 2001
From: happy-game
Date: Thu, 8 May 2025 17:06:14 +0800
Subject: [PATCH 6/8] feat: Set default friendly name using hostname or the URL
host (#5795)
Co-authored-by: Frank Elsinga
---
src/lang/en.json | 1 +
src/pages/EditMonitor.vue | 25 +++++++-
test/e2e/specs/fridendly-name.spec.js | 83 +++++++++++++++++++++++++++
3 files changed, 108 insertions(+), 1 deletion(-)
create mode 100644 test/e2e/specs/fridendly-name.spec.js
diff --git a/src/lang/en.json b/src/lang/en.json
index 009d24ee6..23832f48d 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -64,6 +64,7 @@
"Expected Value": "Expected Value",
"Json Query Expression": "Json Query Expression",
"Friendly Name": "Friendly Name",
+ "defaultFriendlyName": "New Monitor",
"URL": "URL",
"Hostname": "Hostname",
"Host URL": "Host URL",
diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue
index a83f91cab..10d944270 100644
--- a/src/pages/EditMonitor.vue
+++ b/src/pages/EditMonitor.vue
@@ -109,7 +109,7 @@
-
+
@@ -1157,6 +1157,25 @@ export default {
},
computed: {
+ defaultFriendlyName() {
+ if (this.monitor.hostname) {
+ return this.monitor.hostname;
+ }
+ if (this.monitor.url) {
+ if (this.monitor.url !== "http://" && this.monitor.url !== "https://") {
+ // Ensure monitor without a URL is not affected by invisible URL.
+ try {
+ const url = new URL(this.monitor.url);
+ return url.hostname;
+ } catch (e) {
+ return this.monitor.url.replace(/https?:\/\//, "");
+ }
+ }
+ }
+ // Default placeholder if neither hostname nor URL is available
+ return this.$t("defaultFriendlyName");
+ },
+
ipRegex() {
// Allow to test with simple dns server with port (127.0.0.1:5300)
@@ -1700,6 +1719,10 @@ message HealthCheckResponse {
this.processing = true;
+ if (!this.monitor.name) {
+ this.monitor.name = this.defaultFriendlyName;
+ }
+
if (!this.isInputValid()) {
this.processing = false;
return;
diff --git a/test/e2e/specs/fridendly-name.spec.js b/test/e2e/specs/fridendly-name.spec.js
new file mode 100644
index 000000000..7dbe9dd06
--- /dev/null
+++ b/test/e2e/specs/fridendly-name.spec.js
@@ -0,0 +1,83 @@
+import { expect, test } from "@playwright/test";
+import { login, restoreSqliteSnapshot, screenshot } from "../util-test";
+
+test.describe("Friendly Name Tests", () => {
+ test.beforeEach(async ({ page }) => {
+ await restoreSqliteSnapshot(page);
+ });
+
+ test("hostname", async ({ page }, testInfo) => {
+ // Test DNS monitor with hostname
+ await page.goto("./add");
+ await login(page);
+ await screenshot(testInfo, page);
+
+ await page.getByTestId("monitor-type-select").selectOption("dns");
+ await page.getByTestId("hostname-input").fill("example.com");
+ await screenshot(testInfo, page);
+
+ await page.getByTestId("save-button").click();
+ await page.waitForURL("/dashboard/*");
+
+ expect(page.getByTestId("monitor-list")).toContainText("example.com");
+ await screenshot(testInfo, page);
+ });
+
+ test("URL hostname", async ({ page }, testInfo) => {
+ // Test HTTP monitor with URL
+ await page.goto("./add");
+ await login(page);
+ await screenshot(testInfo, page);
+
+ await page.getByTestId("monitor-type-select").selectOption("http");
+ await page.getByTestId("url-input").fill("https://www.example.com/");
+ await screenshot(testInfo, page);
+
+ await page.getByTestId("save-button").click();
+ await page.waitForURL("/dashboard/*");
+
+ expect(page.getByTestId("monitor-list")).toContainText("www.example.com");
+ await screenshot(testInfo, page);
+ });
+
+ test("custom friendly name", async ({ page }, testInfo) => {
+ // Test custom friendly name for HTTP monitor
+ await page.goto("./add");
+ await login(page);
+ await screenshot(testInfo, page);
+
+ await page.getByTestId("monitor-type-select").selectOption("http");
+ await page.getByTestId("url-input").fill("https://www.example.com/");
+
+ // Check if the friendly name placeholder is set to the hostname
+ const friendlyNameInput = page.getByTestId("friendly-name-input");
+ expect(friendlyNameInput).toHaveAttribute("placeholder", "www.example.com");
+ await screenshot(testInfo, page);
+
+ const customName = "Example Monitor";
+ await friendlyNameInput.fill(customName);
+ await screenshot(testInfo, page);
+
+ await page.getByTestId("save-button").click();
+ await page.waitForURL("/dashboard/*");
+
+ expect(page.getByTestId("monitor-list")).toContainText(customName);
+ await screenshot(testInfo, page);
+ });
+
+ test("default friendly name", async ({ page }, testInfo) => {
+ // Test default friendly name when no custom name is provided
+ await page.goto("./add");
+ await login(page);
+ await screenshot(testInfo, page);
+
+ await page.getByTestId("monitor-type-select").selectOption("group");
+ await screenshot(testInfo, page);
+
+ await page.getByTestId("save-button").click();
+ await page.waitForURL("/dashboard/*");
+
+ expect(page.getByTestId("monitor-list")).toContainText("New Monitor");
+ await screenshot(testInfo, page);
+ });
+});
From 2b3f49a26653495964248b91a6631486f13abe29 Mon Sep 17 00:00:00 2001
From: Ionys <9364594+Ionys320@users.noreply.github.com>
Date: Sat, 10 May 2025 19:05:37 +0200
Subject: [PATCH 7/8] Add a public URL field for monitors and uses it on the
status page (#5435)
Co-authored-by: Adam Stachowicz
---
.../2025-05-09-0000-add-custom-url.js | 13 ++++++++++
server/model/group.js | 2 +-
server/model/monitor.js | 2 +-
.../status-page-socket-handler.js | 4 +++
src/components/MonitorSettingDialog.vue | 26 +++++++++++++++++--
src/components/PublicGroupList.vue | 1 +
src/lang/en.json | 2 ++
test/e2e/specs/status-page.spec.js | 13 +++++++++-
8 files changed, 58 insertions(+), 5 deletions(-)
create mode 100644 db/knex_migrations/2025-05-09-0000-add-custom-url.js
diff --git a/db/knex_migrations/2025-05-09-0000-add-custom-url.js b/db/knex_migrations/2025-05-09-0000-add-custom-url.js
new file mode 100644
index 000000000..b3465c87f
--- /dev/null
+++ b/db/knex_migrations/2025-05-09-0000-add-custom-url.js
@@ -0,0 +1,13 @@
+// Add column custom_url to monitor_group table
+exports.up = function (knex) {
+ return knex.schema
+ .alterTable("monitor_group", function (table) {
+ table.text("custom_url", "text");
+ });
+};
+
+exports.down = function (knex) {
+ return knex.schema.alterTable("monitor_group", function (table) {
+ table.dropColumn("custom_url");
+ });
+};
diff --git a/server/model/group.js b/server/model/group.js
index bd2c30189..16c482759 100644
--- a/server/model/group.js
+++ b/server/model/group.js
@@ -33,7 +33,7 @@ class Group extends BeanModel {
*/
async getMonitorList() {
return R.convertToBeans("monitor", await R.getAll(`
- SELECT monitor.*, monitor_group.send_url FROM monitor, monitor_group
+ SELECT monitor.*, monitor_group.send_url, monitor_group.custom_url FROM monitor, monitor_group
WHERE monitor.id = monitor_group.monitor_id
AND group_id = ?
ORDER BY monitor_group.weight
diff --git a/server/model/monitor.js b/server/model/monitor.js
index 58decbcba..85293bbdc 100644
--- a/server/model/monitor.js
+++ b/server/model/monitor.js
@@ -53,7 +53,7 @@ class Monitor extends BeanModel {
};
if (this.sendUrl) {
- obj.url = this.url;
+ obj.url = this.customUrl ?? this.url;
}
if (showTags) {
diff --git a/server/socket-handlers/status-page-socket-handler.js b/server/socket-handlers/status-page-socket-handler.js
index 1114d81fd..952ec2fa7 100644
--- a/server/socket-handlers/status-page-socket-handler.js
+++ b/server/socket-handlers/status-page-socket-handler.js
@@ -211,6 +211,10 @@ module.exports.statusPageSocketHandler = (socket) => {
relationBean.send_url = monitor.sendUrl;
}
+ if (monitor.url !== undefined) {
+ relationBean.custom_url = monitor.url;
+ }
+
await R.store(relationBean);
}
diff --git a/src/components/MonitorSettingDialog.vue b/src/components/MonitorSettingDialog.vue
index e6b2cd1ef..8723c4862 100644
--- a/src/components/MonitorSettingDialog.vue
+++ b/src/components/MonitorSettingDialog.vue
@@ -10,7 +10,7 @@
+
+
+
+ changeUrl(monitor.group_index, monitor.monitor_index, e.target!.value)">
+
+
+ {{ $t("customUrlDescription") }}
+
+
+