From 59f6df50c5bf71e6b90efbe7cd33348122948e56 Mon Sep 17 00:00:00 2001 From: "Valentin V. Bartenev" Date: Sat, 16 May 2026 01:51:06 +0300 Subject: [PATCH] Web: replace unreliable post-OTA device poll with a countdown timer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After a successful OTA upload the web panel polled the device by fetching "/" and waiting for a 200 OK response. This approach is fundamentally broken when the new firmware contains a freshly generated self-signed certificate (e.g. from a clean CI build): - The browser rejects the TLS handshake with a certificate error (MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT in Firefox; other browsers report different error names but behave identically). The fetch() call throws a generic TypeError — the same type thrown for any other network failure such as a refused or timed-out connection. Browsers intentionally provide no way to distinguish a cert rejection from a plain connectivity failure, so the catch block cannot tell whether the device is up-but-cert-changed or simply not yet online. - Switching to an explicit http:// URL to sidestep TLS is not an option: the web panel is served over HTTPS, and browsers block active mixed content (fetch/XHR to HTTP) unconditionally from an HTTPS origin. Because no network-based probe can reliably detect device readiness under these constraints without changing how the certificate is generated, replace the polling loop with a simple 10-second countdown timer that unconditionally redirects to the login page. The device is typically back online within ~7 seconds, so the 10-second wait provides a reasonable safety margin. --- src/helpers/web/WebPanelServer.cpp | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/helpers/web/WebPanelServer.cpp b/src/helpers/web/WebPanelServer.cpp index b3805279..d3488054 100644 --- a/src/helpers/web/WebPanelServer.cpp +++ b/src/helpers/web/WebPanelServer.cpp @@ -2641,22 +2641,14 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML( } if (xhr.status === 200) { barEl.style.width = "100%"; - msgEl.textContent = "Done. Device is rebooting"; - let wait = 4; - const iv = setInterval(async () => { - msgEl.textContent += '.'; - if (msgEl.textContent.length > 85) { clearInterval(iv); msgEl.textContent = "Device did not come back online. =("; return; } - if (--wait) { return; } - wait = 1000; // prevent concurrent fetch - try { - if ((await fetch("/", { cache: "no-store" })).ok) { - clearInterval(iv); - msgEl.textContent = "Device is back online. Redirecting to login..."; - setTimeout(redirectToLogin, 1500); - return; - } - } catch (_) {} - wait = 4; + let secs = 10; + const iv = setInterval(() => { + if (--secs <= 0) { + clearInterval(iv); + redirectToLogin(); + return; + } + msgEl.textContent = `Done. Device is rebooting. Redirecting in ${secs}s...`; }, 1000); } else { msgEl.textContent = "Failed: " + (xhr.responseText || String(xhr.status));