diff --git a/db/knex_migrations/2025-07-17-0000-mqtt-websocket-path.js b/db/knex_migrations/2025-07-17-0000-mqtt-websocket-path.js new file mode 100644 index 000000000..85b05f110 --- /dev/null +++ b/db/knex_migrations/2025-07-17-0000-mqtt-websocket-path.js @@ -0,0 +1,15 @@ +exports.up = function (knex) { + // Add new column monitor.mqtt_websocket_path + return knex.schema + .alterTable("monitor", function (table) { + table.string("mqtt_websocket_path", 255).nullable(); + }); +}; + +exports.down = function (knex) { + // Drop column monitor.mqtt_websocket_path + return knex.schema + .alterTable("monitor", function (table) { + table.dropColumn("mqtt_websocket_path"); + }); +}; diff --git a/server/model/monitor.js b/server/model/monitor.js index bfc235607..2c0bde5b7 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -189,6 +189,7 @@ class Monitor extends BeanModel { radiusSecret: this.radiusSecret, mqttUsername: this.mqttUsername, mqttPassword: this.mqttPassword, + mqttWebsocketPath: this.mqttWebsocketPath, authWorkstation: this.authWorkstation, authDomain: this.authDomain, tlsCa: this.tlsCa, diff --git a/server/monitor-types/mqtt.js b/server/monitor-types/mqtt.js index ad734ce8e..1865bbb42 100644 --- a/server/monitor-types/mqtt.js +++ b/server/monitor-types/mqtt.js @@ -15,6 +15,7 @@ class MqttMonitorType extends MonitorType { username: monitor.mqttUsername, password: monitor.mqttPassword, interval: monitor.interval, + websocketPath: monitor.mqttWebsocketPath, }); if (monitor.mqttCheckType == null || monitor.mqttCheckType === "") { @@ -52,12 +53,12 @@ class MqttMonitorType extends MonitorType { * @param {string} hostname Hostname / address of machine to test * @param {string} topic MQTT topic * @param {object} options MQTT options. Contains port, username, - * password and interval (interval defaults to 20) + * password, websocketPath and interval (interval defaults to 20) * @returns {Promise} Received MQTT message */ mqttAsync(hostname, topic, options = {}) { return new Promise((resolve, reject) => { - const { port, username, password, interval = 20 } = options; + const { port, username, password, websocketPath, interval = 20 } = options; // Adds MQTT protocol to the hostname if not already present if (!/^(?:http|mqtt|ws)s?:\/\//.test(hostname)) { @@ -70,7 +71,15 @@ class MqttMonitorType extends MonitorType { reject(new Error("Timeout, Message not received")); }, interval * 1000 * 0.8); - const mqttUrl = `${hostname}:${port}`; + // Construct the URL based on protocol + let mqttUrl = `${hostname}:${port}`; + if (hostname.startsWith("ws://") || hostname.startsWith("wss://")) { + if (websocketPath && !websocketPath.startsWith("/")) { + mqttUrl = `${hostname}:${port}/${websocketPath || ""}`; + } else { + mqttUrl = `${hostname}:${port}${websocketPath || ""}`; + } + } log.debug("mqtt", `MQTT connecting to ${mqttUrl}`); diff --git a/server/server.js b/server/server.js index b7025464b..55289b55a 100644 --- a/server/server.js +++ b/server/server.js @@ -720,6 +720,17 @@ let needSetup = false; monitor.rabbitmqNodes = JSON.stringify(monitor.rabbitmqNodes); + /* + * List of frontend-only properties that should not be saved to the database. + * Should clean up before saving to the database. + */ + const frontendOnlyProperties = [ "humanReadableInterval" ]; + for (const prop of frontendOnlyProperties) { + if (prop in monitor) { + delete monitor[prop]; + } + } + bean.import(monitor); bean.user_id = socket.userID; @@ -837,6 +848,7 @@ let needSetup = false; bean.mqttTopic = monitor.mqttTopic; bean.mqttSuccessMessage = monitor.mqttSuccessMessage; bean.mqttCheckType = monitor.mqttCheckType; + bean.mqttWebsocketPath = monitor.mqttWebsocketPath; bean.databaseConnectionString = monitor.databaseConnectionString; bean.databaseQuery = monitor.databaseQuery; bean.authMethod = monitor.authMethod; diff --git a/src/lang/en.json b/src/lang/en.json index b6449371b..2331e29dc 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -71,6 +71,7 @@ "locally configured mail transfer agent": "locally configured mail transfer agent", "Either enter the hostname of the server you want to connect to or localhost if you intend to use a locally configured mail transfer agent": "Either enter the hostname of the server you want to connect to or {localhost} if you intend to use a {local_mta}", "Port": "Port", + "Path": "Path", "Heartbeat Interval": "Heartbeat Interval", "Request Timeout": "Request Timeout", "timeoutAfter": "Timeout after {0} seconds", @@ -266,6 +267,10 @@ "Current User": "Current User", "topic": "Topic", "topicExplanation": "MQTT topic to monitor", + "mqttWebSocketPath": "MQTT WebSocket Path", + "mqttWebsocketPathExplanation": "WebSocket path for MQTT over WebSocket connections (e.g., /mqtt)", + "mqttWebsocketPathInvalid": "Please use a valid WebSocket Path format", + "mqttHostnameTip": "Please use this format {hostnameFormat}", "successKeyword": "Success Keyword", "successKeywordExplanation": "MQTT Keyword that will be considered as success", "recent": "Recent", diff --git a/src/mixins/lang.js b/src/mixins/lang.js index 9061e7d3d..0fff8cdc8 100644 --- a/src/mixins/lang.js +++ b/src/mixins/lang.js @@ -1,5 +1,5 @@ import { currentLocale } from "../i18n"; -import { setPageLocale } from "../util-frontend"; +import { setPageLocale, relativeTimeFormatter } from "../util-frontend"; const langModules = import.meta.glob("../lang/*.json"); export default { @@ -28,11 +28,13 @@ export default { * @returns {Promise} */ async changeLang(lang) { - let message = (await langModules["../lang/" + lang + ".json"]()).default; + let message = (await langModules["../lang/" + lang + ".json"]()) + .default; this.$i18n.setLocaleMessage(lang, message); this.$i18n.locale = lang; localStorage.locale = lang; setPageLocale(); - } - } + relativeTimeFormatter.updateLocale(lang); + }, + }, }; diff --git a/src/pages/Details.vue b/src/pages/Details.vue index 1d068b92e..732d58e31 100644 --- a/src/pages/Details.vue +++ b/src/pages/Details.vue @@ -1,7 +1,9 @@