From 9df8505c8d7918a8b1f66b4a5e58a252f4cd6afc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 19 Aug 2026 13:25:51 -0500 Subject: [PATCH] [web_server] Show the offline hint on a timer so a stalled download is covered too --- esphome/components/web_server/__init__.py | 30 +++++++++++-------- .../unit_tests/components/test_web_server.py | 17 +++++++---- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index bfa3cac5a1..f0aaac9b5e 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -362,7 +362,16 @@ async def add_entity_config(entity, config): def build_index_html(config: ConfigType, offline_hint: bool = True) -> str: + js_url = config[CONF_JS_URL] + # The interface is downloaded from the internet. Without internet access, which is + # common for browsers on the WiFi AP, that download fails or simply stalls and the + # page stays blank. Show a hint after a few seconds unless www.js has registered the + # esp-app element; CSS hides the hint again if the interface does arrive later. + # Kept short: it lives in flash on every build without captive_portal. + offline_hint = offline_hint and bool(js_url) html = "" + if offline_hint: + html += "" css_include = config.get(CONF_CSS_INCLUDE) js_include = config.get(CONF_JS_INCLUDE) if css_include: @@ -373,18 +382,15 @@ def build_index_html(config: ConfigType, offline_hint: bool = True) -> str: if js_include: html += "" html += "" - if js_url := config[CONF_JS_URL]: - onerror = "" - if offline_hint: - # The interface is downloaded from the internet. Show a hint instead of a - # blank page when the browser cannot reach it, which is common in WiFi AP - # mode. Kept short: it lives in flash on every build without captive_portal. - onerror = ( - " onerror=\"document.body.innerText='Could not download the web interface. " - "This browser needs internet access, or set local: true under web_server: " - "in the YAML.'\"" - ) - html += f'' + if offline_hint: + html += ( + "" + "" + ) + if js_url: + html += f'' html += "" return html diff --git a/tests/unit_tests/components/test_web_server.py b/tests/unit_tests/components/test_web_server.py index dc0fb03d21..55c26b0dc7 100644 --- a/tests/unit_tests/components/test_web_server.py +++ b/tests/unit_tests/components/test_web_server.py @@ -23,24 +23,29 @@ JS_URL = "https://oi.esphome.io/v2/www.js" def test_build_index_html_has_offline_hint() -> None: - """The hosted script tag shows a hint when the browser cannot download it.""" + """A hidden hint is shown after a timeout unless www.js registered esp-app.""" html = build_index_html({CONF_JS_URL: JS_URL, CONF_CSS_URL: ""}) - assert f'' in html - assert "onerror" not in html + assert f'' in html + assert "" not in html def test_build_index_html_without_js_url() -> None: """No hosted script and no hint when js_url is empty.""" html = build_index_html({CONF_JS_URL: "", CONF_CSS_URL: ""}) assert "