[web_server] Show the offline hint on a timer so a stalled download is covered too

This commit is contained in:
J. Nick Koston
2026-08-19 13:25:51 -05:00
parent 427b83dc0a
commit 9df8505c8d
2 changed files with 29 additions and 18 deletions
+18 -12
View File
@@ -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 = "<!DOCTYPE html><html><head><meta charset=UTF-8><link rel=icon href=data:>"
if offline_hint:
html += "<style>esp-app:defined+p{display:none}</style>"
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 += "<script type=module src=/0.js></script>"
html += "<esp-app></esp-app>"
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'<script src="{js_url}"{onerror}></script>'
if offline_hint:
html += (
"<p hidden>The web interface is not loading. This browser needs internet "
"access to download it, or set local: true under web_server: in the YAML.</p>"
"<script>setTimeout(function(){document.querySelector('p').hidden=0},5e3)"
"</script>"
)
if js_url:
html += f'<script src="{js_url}"></script>'
html += "</body></html>"
return html
+11 -6
View File
@@ -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'<script src="{JS_URL}" onerror="' in html
assert "document.body.innerText='Could not download the web interface." in html
assert "<style>esp-app:defined+p{display:none}</style>" in html
assert "<esp-app></esp-app><p hidden>The web interface is not loading." in html
assert "local: true" in html
assert "<script>setTimeout(function(){document.querySelector('p').hidden=0}" in html
# The hint script must come before the hosted script, which may never finish loading
assert html.index("setTimeout") < html.index(f'<script src="{JS_URL}">')
def test_build_index_html_hint_disabled() -> None:
"""The plain script tag is kept when the hint is not wanted (captive_portal)."""
"""Plain page when the hint is not wanted (captive_portal)."""
html = build_index_html({CONF_JS_URL: JS_URL, CONF_CSS_URL: ""}, offline_hint=False)
assert f'<script src="{JS_URL}"></script>' in html
assert "onerror" not in html
assert f'<esp-app></esp-app><script src="{JS_URL}"></script>' in html
assert "<p" not in html
assert "<style>" 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 "<script" not in html
assert "<p" not in html
@pytest.mark.parametrize(