fix: preserve web panel session across Wi-Fi reconnects

This commit is contained in:
Jared Dohrman committed 2026-05-25 10:24:16 +10:00
1 parent 4aa2a3dabd
commit 0fcb010a6f
4 files changed
+62 -27

No files matched your search

+3 -2
View File
@@ -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.
+53 -23
View File
@@ -1005,22 +1005,12 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
</div>
</div>
<div class="broker-card">
<div class="row">
<div class="field-card">
<label class="label" for="mqttCustomHost">Host</label>
<div class="inline-actions">
<input id="mqttCustomHost" placeholder="mqtt.example.net" maxlength="95">
<button class="iconbtn" data-load-cmd="get mqtt.custom.host" data-load-input="mqttCustomHost" title="Refresh custom MQTT host">&#8635;</button>
<button class="savebtn" data-prefix="set mqtt.custom.host " data-input="mqttCustomHost">Save</button>
</div>
</div>
<div class="field-card">
<label class="label" for="mqttCustomPort">Port</label>
<div class="inline-actions">
<input id="mqttCustomPort" inputmode="numeric" placeholder="1883" maxlength="5">
<button class="iconbtn" data-load-cmd="get mqtt.custom.port" data-load-input="mqttCustomPort" title="Refresh custom MQTT port">&#8635;</button>
<button class="savebtn" data-prefix="set mqtt.custom.port " data-input="mqttCustomPort">Save</button>
</div>
<div class="field-card">
<label class="label" for="mqttCustomEndpoint">Host:Port</label>
<div class="inline-actions">
<input id="mqttCustomEndpoint" placeholder="mqtt.xnet.wtf:1883" maxlength="101">
<button id="refreshCustomEndpointBtn" class="iconbtn" title="Refresh custom MQTT host and port">&#8635;</button>
<button id="saveCustomEndpointBtn" class="savebtn">Save</button>
</div>
</div>
<div class="row">
@@ -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() {
+1 -1
View File
@@ -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;
+5 -1
View File
@@ -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;
}