diff --git a/eastmesh-docs/web-panel.md b/eastmesh-docs/web-panel.md index 510a46ca..6dfd46b2 100644 --- a/eastmesh-docs/web-panel.md +++ b/eastmesh-docs/web-panel.md @@ -108,7 +108,8 @@ Example: - the connection is HTTPS, but the certificate is self-signed - browsers will warn the first time you connect - the panel exposes the repeater CLI after login -- the browser keeps the session token while changing between `/app` and `/stats`; the page switches views in place until logout, idle lock, or server restart +- the browser keeps the session token while changing between `/app` and `/stats`; the page switches views in place until logout, idle lock, or device restart +- brief Wi-Fi drops can interrupt in-flight page loads, but should not force a new login when the web panel restarts - after a device restart or reflash, the app checks the stored token before loading settings and returns to login if the session is stale This is intended for local admin use on a trusted network, not for open internet exposure. @@ -248,7 +249,7 @@ This section includes: - `mqtt.owner`: owner public key. - `mqtt.email`: owner contact email. - MQTT server toggles: `eastmesh-au`, `letsmesh-eu`, `letsmesh-us`, and custom MQTT. -- custom MQTT host, port, username, and password fields. +- custom MQTT `host:port`, username, and password fields. `UNSET - To be configured` is the default for new observer installs until a real saved value exists. diff --git a/src/helpers/web/WebPanelServer.cpp b/src/helpers/web/WebPanelServer.cpp index 21b9eb02..fd20ca46 100644 --- a/src/helpers/web/WebPanelServer.cpp +++ b/src/helpers/web/WebPanelServer.cpp @@ -1005,22 +1005,12 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
-
-
- -
- - - -
-
-
- -
- - - -
+
+ +
+ + +
@@ -2325,7 +2315,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML( } else if (format === "uppercase") { value = value.toUpperCase(); } - if ((inputId === "mqttCustomHost" || inputId === "mqttCustomUsername") && value === "-") { + if (inputId === "mqttCustomUsername" && value === "-") { value = ""; } document.getElementById(inputId).value = value; @@ -2492,6 +2482,43 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML( if (!result.ok) return; setBrokerToggle(inputId, parseReplyValue(result.text)); } + async function loadCustomEndpoint(options = {}) { + const hostResult = await runCommand("get mqtt.custom.host", options); + if (!hostResult.ok) return; + const portResult = await runCommand("get mqtt.custom.port", options); + if (!portResult.ok) return; + const host = parseReplyValue(hostResult.text); + const port = parseReplyValue(portResult.text) || "1883"; + const input = document.getElementById("mqttCustomEndpoint"); + if (input) { + input.value = host && host !== "-" ? `${host}:${port}` : ""; + } + } + function parseCustomEndpoint(value) { + const endpoint = String(value || "").trim(); + const separator = endpoint.lastIndexOf(":"); + if (separator <= 0 || separator === endpoint.length - 1) return null; + const host = endpoint.slice(0, separator).trim(); + const port = endpoint.slice(separator + 1).trim(); + if (!host || !/^[0-9]+$/.test(port)) return null; + const portNumber = Number.parseInt(port, 10); + if (!Number.isFinite(portNumber) || portNumber < 1 || portNumber > 65535) return null; + return { host, port:String(portNumber) }; + } + async function saveCustomEndpoint() { + const input = document.getElementById("mqttCustomEndpoint"); + if (!input) return; + const parsed = parseCustomEndpoint(input.value); + if (!parsed) { + statusEl.textContent = "Use host:port, for example mqtt.xnet.wtf:1883"; + return; + } + const hostResult = await runCommand("set mqtt.custom.host " + parsed.host); + if (!hostResult.ok) return; + const portResult = await runCommand("set mqtt.custom.port " + parsed.port); + if (!portResult.ok) return; + input.value = `${parsed.host}:${parsed.port}`; + } async function loadRadioConfig(options = {}) { const result = await runCommand("get radio", options); if (!result.ok) { @@ -2669,6 +2696,8 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML( setCustomMqttMode((Number.parseInt(customMqttModeSlider.value, 10) || 0) >= 1); }); } + document.getElementById("refreshCustomEndpointBtn").onclick = () => loadCustomEndpoint(); + document.getElementById("saveCustomEndpointBtn").onclick = () => saveCustomEndpoint(); async function setLetsmeshMode(mode) { const eastmesh = document.getElementById("mqttEastmeshAu"); if (mode === "both" && eastmesh && eastmesh.checked) { @@ -2859,8 +2888,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML( () => loadBrokerState("get mqtt.letsmesh-eu", "mqttLetsmeshEu", quiet), () => loadBrokerState("get mqtt.letsmesh-us", "mqttLetsmeshUs", quiet), () => loadBrokerState("get mqtt.custom", "mqttCustom", quiet), - () => loadField("get mqtt.custom.host", "mqttCustomHost", null, quiet), - () => loadField("get mqtt.custom.port", "mqttCustomPort", null, quiet), + () => loadCustomEndpoint(quiet), () => loadField("get mqtt.custom.username", "mqttCustomUsername", null, quiet) ]); if (!isCurrentPageLoad(generation)) return; @@ -2966,7 +2994,7 @@ bool WebPanelServer::start() { return true; } -void WebPanelServer::stop() { +void WebPanelServer::stop(bool clear_session) { stopRedirectServer(); if (_server != nullptr) { httpd_handle_t server = _server; @@ -2974,8 +3002,10 @@ void WebPanelServer::stop() { WEB_PANEL_LOG("server stopped"); httpd_ssl_stop(server); } - _token[0] = 0; - _last_activity_ms = 0; + if (clear_session) { + _token[0] = 0; + _last_activity_ms = 0; + } } bool WebPanelServer::isRunning() const { @@ -3242,7 +3272,7 @@ bool WebPanelServer::start() { return false; } -void WebPanelServer::stop() { +void WebPanelServer::stop(bool) { } void WebPanelServer::stopRedirectServer() { diff --git a/src/helpers/web/WebPanelServer.h b/src/helpers/web/WebPanelServer.h index 73918e50..ff172d48 100644 --- a/src/helpers/web/WebPanelServer.h +++ b/src/helpers/web/WebPanelServer.h @@ -40,7 +40,7 @@ public: void setCommandRunner(WebPanelCommandRunner* runner); bool start(); - void stop(); + void stop(bool clear_session = true); void stopRedirectServer(); bool isRunning() const; bool hasSessionToken() const; diff --git a/src/helpers/web/WebService.cpp b/src/helpers/web/WebService.cpp index f0323dd4..6a8b8856 100644 --- a/src/helpers/web/WebService.cpp +++ b/src/helpers/web/WebService.cpp @@ -104,10 +104,14 @@ void WebService::ensureWebServer() { } return; } - if (_runner == nullptr || _prefs.web_enabled == 0 || _network == nullptr || !_network->isWifiConnected()) { + if (_runner == nullptr || _prefs.web_enabled == 0) { _panel.stop(); return; } + if (_network == nullptr || !_network->isWifiConnected()) { + _panel.stop(false); + return; + } if (_panel.isRunning()) { return; }