diff --git a/db/knex_migrations/2025-06-03-0000-add-ip-family.js b/db/knex_migrations/2025-06-03-0000-add-ip-family.js
new file mode 100644
index 000000000..a3bcdc613
--- /dev/null
+++ b/db/knex_migrations/2025-06-03-0000-add-ip-family.js
@@ -0,0 +1,13 @@
+exports.up = function (knex) {
+ return knex.schema
+ .alterTable("monitor", function (table) {
+ table.boolean("ip_family").defaultTo(null);
+ });
+};
+
+exports.down = function (knex) {
+ return knex.schema
+ .alterTable("monitor", function (table) {
+ table.dropColumn("ip_family");
+ });
+};
diff --git a/db/knex_migrations/2025-06-11-0000-add-manual-monitor.js b/db/knex_migrations/2025-06-11-0000-add-manual-monitor.js
new file mode 100644
index 000000000..16d307eb5
--- /dev/null
+++ b/db/knex_migrations/2025-06-11-0000-add-manual-monitor.js
@@ -0,0 +1,12 @@
+exports.up = function (knex) {
+ return knex.schema
+ .alterTable("monitor", function (table) {
+ table.string("manual_status").defaultTo(null);
+ });
+};
+
+exports.down = function (knex) {
+ return knex.schema.alterTable("monitor", function (table) {
+ table.dropColumn("manual_status");
+ });
+};
diff --git a/server/model/maintenance.js b/server/model/maintenance.js
index 828c1f757..7ae88abd0 100644
--- a/server/model/maintenance.js
+++ b/server/model/maintenance.js
@@ -158,12 +158,22 @@ class Maintenance extends BeanModel {
bean.active = obj.active;
if (obj.dateRange[0]) {
+ const parsedDate = new Date(obj.dateRange[0]);
+ if (isNaN(parsedDate.getTime()) || parsedDate.getFullYear() > 9999) {
+ throw new Error("Invalid start date");
+ }
+
bean.start_date = obj.dateRange[0];
} else {
bean.start_date = null;
}
if (obj.dateRange[1]) {
+ const parsedDate = new Date(obj.dateRange[1]);
+ if (isNaN(parsedDate.getTime()) || parsedDate.getFullYear() > 9999) {
+ throw new Error("Invalid end date");
+ }
+
bean.end_date = obj.dateRange[1];
} else {
bean.end_date = null;
@@ -235,7 +245,7 @@ class Maintenance extends BeanModel {
try {
this.beanMeta.status = "scheduled";
- let startEvent = (customDuration = 0) => {
+ let startEvent = async (customDuration = 0) => {
log.info("maintenance", "Maintenance id: " + this.id + " is under maintenance now");
this.beanMeta.status = "under-maintenance";
diff --git a/server/model/monitor.js b/server/model/monitor.js
index 741fb940e..c9844a55d 100644
--- a/server/model/monitor.js
+++ b/server/model/monitor.js
@@ -160,6 +160,7 @@ class Monitor extends BeanModel {
smtpSecurity: this.smtpSecurity,
rabbitmqNodes: JSON.parse(this.rabbitmqNodes),
conditions: JSON.parse(this.conditions),
+ ipFamily: this.ipFamily,
// ping advanced options
ping_numeric: this.isPingNumeric(),
@@ -426,10 +427,26 @@ class Monitor extends BeanModel {
}
}
+ let agentFamily = undefined;
+ if (this.ipFamily === "ipv4") {
+ agentFamily = 4;
+ }
+ if (this.ipFamily === "ipv6") {
+ agentFamily = 6;
+ }
+
const httpsAgentOptions = {
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: !this.getIgnoreTls(),
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
+ autoSelectFamily: true,
+ ...(agentFamily ? { family: agentFamily } : {})
+ };
+
+ const httpAgentOptions = {
+ maxCachedSessions: 0,
+ autoSelectFamily: true,
+ ...(agentFamily ? { family: agentFamily } : {})
};
log.debug("monitor", `[${this.name}] Prepare Options for axios`);
@@ -491,6 +508,7 @@ class Monitor extends BeanModel {
if (proxy && proxy.active) {
const { httpAgent, httpsAgent } = Proxy.createAgents(proxy, {
httpsAgentOptions: httpsAgentOptions,
+ httpAgentOptions: httpAgentOptions,
});
options.proxy = false;
@@ -499,6 +517,10 @@ class Monitor extends BeanModel {
}
}
+ if (!options.httpAgent) {
+ options.httpAgent = new http.Agent(httpAgentOptions);
+ }
+
if (!options.httpsAgent) {
let jar = new CookieJar();
let httpsCookieAgentOptions = {
diff --git a/server/model/status_page.js b/server/model/status_page.js
index 38f548ebb..2f3511ec5 100644
--- a/server/model/status_page.js
+++ b/server/model/status_page.js
@@ -120,8 +120,8 @@ class StatusPage extends BeanModel {
const head = $("head");
- if (statusPage.googleAnalyticsTagId) {
- let escapedGoogleAnalyticsScript = googleAnalytics.getGoogleAnalyticsScript(statusPage.googleAnalyticsTagId);
+ if (statusPage.google_analytics_tag_id) {
+ let escapedGoogleAnalyticsScript = googleAnalytics.getGoogleAnalyticsScript(statusPage.google_analytics_tag_id);
head.append($(escapedGoogleAnalyticsScript));
}
diff --git a/server/modules/axios-ntlm/lib/ntlmClient.js b/server/modules/axios-ntlm/lib/ntlmClient.js
index 682de5f9a..9dab32553 100644
--- a/server/modules/axios-ntlm/lib/ntlmClient.js
+++ b/server/modules/axios-ntlm/lib/ntlmClient.js
@@ -89,6 +89,9 @@ function NtlmClient(credentials, AxiosConfig) {
switch (_b.label) {
case 0:
error = err.response;
+ // The header may look like this: `Negotiate, NTLM, Basic realm="itsahiddenrealm.example.net"`Add commentMore actions
+ // so extract the 'NTLM' part first
+ const ntlmheader = error.headers['www-authenticate'].split(',').find(_ => _.match(/ *NTLM/))?.trim() || '';
if (!(error && error.status === 401
&& error.headers['www-authenticate']
&& error.headers['www-authenticate'].includes('NTLM'))) return [3 /*break*/, 3];
@@ -96,12 +99,12 @@ function NtlmClient(credentials, AxiosConfig) {
// include the Negotiate option when responding with the T2 message
// There is nore we could do to ensure we are processing correctly,
// but this is the easiest option for now
- if (error.headers['www-authenticate'].length < 50) {
+ if (ntlmheader.length < 50) {
t1Msg = ntlm.createType1Message(credentials.workstation, credentials.domain);
error.config.headers["Authorization"] = t1Msg;
}
else {
- t2Msg = ntlm.decodeType2Message((error.headers['www-authenticate'].match(/^NTLM\s+(.+?)(,|\s+|$)/) || [])[1]);
+ t2Msg = ntlm.decodeType2Message((ntlmheader.match(/^NTLM\s+(.+?)(,|\s+|$)/) || [])[1]);
t3Msg = ntlm.createType3Message(t2Msg, credentials.username, credentials.password, credentials.workstation, credentials.domain);
error.config.headers["X-retry"] = "false";
error.config.headers["Authorization"] = t3Msg;
diff --git a/server/monitor-types/manual.js b/server/monitor-types/manual.js
new file mode 100644
index 000000000..e587b7409
--- /dev/null
+++ b/server/monitor-types/manual.js
@@ -0,0 +1,36 @@
+const { MonitorType } = require("./monitor-type");
+const { UP, DOWN, PENDING } = require("../../src/util");
+
+class ManualMonitorType extends MonitorType {
+ name = "Manual";
+ type = "manual";
+ description = "A monitor that allows manual control of the status";
+ supportsConditions = false;
+ conditionVariables = [];
+
+ /**
+ * @inheritdoc
+ */
+ async check(monitor, heartbeat) {
+ if (monitor.manual_status !== null) {
+ heartbeat.status = monitor.manual_status;
+ switch (monitor.manual_status) {
+ case UP:
+ heartbeat.msg = "Up";
+ break;
+ case DOWN:
+ heartbeat.msg = "Down";
+ break;
+ default:
+ heartbeat.msg = "Pending";
+ }
+ } else {
+ heartbeat.status = PENDING;
+ heartbeat.msg = "Manual monitoring - No status set";
+ }
+ }
+}
+
+module.exports = {
+ ManualMonitorType
+};
diff --git a/server/notification-providers/ntfy.js b/server/notification-providers/ntfy.js
index ad1d39f8f..e44e7e868 100644
--- a/server/notification-providers/ntfy.js
+++ b/server/notification-providers/ntfy.js
@@ -41,8 +41,8 @@ class Ntfy extends NotificationProvider {
if (heartbeatJSON.status === DOWN) {
tags = [ "red_circle" ];
status = "Down";
- // if priority is not 5, increase priority for down alerts
- priority = priority === 5 ? priority : priority + 1;
+ // defaults to max(priority + 1, 5)
+ priority = notification.ntfyPriorityDown || (priority === 5 ? priority : priority + 1);
} else if (heartbeatJSON["status"] === UP) {
tags = [ "green_circle" ];
status = "Up";
diff --git a/server/prometheus.js b/server/prometheus.js
index f26125d2c..485dfe53a 100644
--- a/server/prometheus.js
+++ b/server/prometheus.js
@@ -2,6 +2,7 @@ const PrometheusClient = require("prom-client");
const { log } = require("../src/util");
const commonLabels = [
+ "monitor_id",
"monitor_name",
"monitor_type",
"monitor_url",
@@ -40,6 +41,7 @@ class Prometheus {
*/
constructor(monitor) {
this.monitorLabelValues = {
+ monitor_id: monitor.id,
monitor_name: monitor.name,
monitor_type: monitor.type,
monitor_url: monitor.url,
diff --git a/server/server.js b/server/server.js
index cba02174d..e328ff470 100644
--- a/server/server.js
+++ b/server/server.js
@@ -792,6 +792,7 @@ let needSetup = false;
bean.url = monitor.url;
bean.method = monitor.method;
bean.body = monitor.body;
+ bean.ipFamily = monitor.ipFamily;
bean.headers = monitor.headers;
bean.basic_auth_user = monitor.basic_auth_user;
bean.basic_auth_pass = monitor.basic_auth_pass;
@@ -875,6 +876,7 @@ let needSetup = false;
bean.rabbitmqUsername = monitor.rabbitmqUsername;
bean.rabbitmqPassword = monitor.rabbitmqPassword;
bean.conditions = JSON.stringify(monitor.conditions);
+ bean.manual_status = monitor.manual_status;
// ping advanced options
bean.ping_numeric = monitor.ping_numeric;
diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js
index 1f75b72cc..a04e6bd49 100644
--- a/server/uptime-kuma-server.js
+++ b/server/uptime-kuma-server.js
@@ -118,6 +118,7 @@ class UptimeKumaServer {
UptimeKumaServer.monitorTypeList["snmp"] = new SNMPMonitorType();
UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType();
UptimeKumaServer.monitorTypeList["rabbitmq"] = new RabbitMqMonitorType();
+ UptimeKumaServer.monitorTypeList["manual"] = new ManualMonitorType();
// Allow all CORS origins (polling) in development
let cors = undefined;
@@ -558,4 +559,5 @@ const { GroupMonitorType } = require("./monitor-types/group");
const { SNMPMonitorType } = require("./monitor-types/snmp");
const { MongodbMonitorType } = require("./monitor-types/mongodb");
const { RabbitMqMonitorType } = require("./monitor-types/rabbitmq");
+const { ManualMonitorType } = require("./monitor-types/manual");
const Monitor = require("./model/monitor");
diff --git a/src/components/TagsManager.vue b/src/components/TagsManager.vue
index aa8f93a83..e7e370a4c 100644
--- a/src/components/TagsManager.vue
+++ b/src/components/TagsManager.vue
@@ -4,7 +4,7 @@
@@ -20,10 +20,20 @@
{{ $t("Add") }}
-
+
+
{{ $t("Add Tags") }}
+
+
+
+
-
- {{ $t("Tag with this name already exist.") }}
-
-
-
+
+
+ {{ $t(validateDraftTag.messageKey, validateDraftTag.messageParams) }}
+
@@ -176,71 +180,146 @@ export default {
newTags: [],
/** @type {Tag[]} */
deleteTags: [],
+ /**
+ * @type {Array
diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue
index 95b29aa58..0d628895d 100644
--- a/src/pages/EditMonitor.vue
+++ b/src/pages/EditMonitor.vue
@@ -55,6 +55,9 @@
+