diff --git a/dockerfiles/Dockerfile.hermes-webui b/dockerfiles/Dockerfile.hermes-webui index f226ad41..5ebf296b 100644 --- a/dockerfiles/Dockerfile.hermes-webui +++ b/dockerfiles/Dockerfile.hermes-webui @@ -84,6 +84,7 @@ RUN /opt/hermes/.venv/bin/python -c 'import cryptography, yaml' \ && grep -Fq "TELEGRAM_PROJECT_NAME = 'Telegram'" /opt/hermes-webui/api/models.py \ && grep -Fq "'atlas/auto/maximum': 'Automatic ยท Maximum'" /opt/hermes-webui/static/panels.js \ && grep -Fq 'Atlas Jetson (private)' /opt/hermes-webui/static/index.html \ + && grep -Fq 'user-scalable=no, viewport-fit=cover' /opt/hermes-webui/static/index.html \ && grep -Fq 'HERMES_WEBUI_ATLAS_TTS_URL' /opt/hermes-webui/api/routes.py \ && grep -Fq 'settings["webui_bundle_version"]' /opt/hermes-webui/api/routes.py \ && grep -Fq 'settings.webui_bundle_version||settings.webui_version' /opt/hermes-webui/static/panels.js \ diff --git a/dockerfiles/hermes-webui-base-patch.py b/dockerfiles/hermes-webui-base-patch.py index 1629d25a..2275cfc1 100644 --- a/dockerfiles/hermes-webui-base-patch.py +++ b/dockerfiles/hermes-webui-base-patch.py @@ -21,6 +21,26 @@ if source.count(before) != 1: raise SystemExit("Hermes WebUI xhigh UI patch context changed") index.write_text(source.replace(before, "", 1), encoding="utf-8") +# Enable edge-to-edge safe-area insets on mobile / standalone PWAs. The theme +# already pads the titlebar with env(safe-area-inset-top/left/right) and the +# composer reserves env(safe-area-inset-bottom) (hermes-brand.css), but env() +# resolves to 0 unless the viewport opts into viewport-fit=cover. Android +# standalone PWAs render the webview edge-to-edge regardless, so without cover +# there is no inset to pad with and the composer's control row hides behind the +# gesture bar. Opt in so those insets carry real values. +source = index.read_text(encoding="utf-8") +before = ( + '' +) +after = ( + '' +) +if source.count(before) != 1: + raise SystemExit("Hermes WebUI viewport-fit patch context changed") +index.write_text(source.replace(before, after, 1), encoding="utf-8") + # oauth2-proxy returns 401 for browser API and health probes when the secure # session expires. Re-enter OIDC with the complete return path. ui = Path("/opt/hermes-webui/static/ui.js") diff --git a/dockerfiles/hermes-webui-brand.css b/dockerfiles/hermes-webui-brand.css index 16ae423f..fa9302eb 100644 --- a/dockerfiles/hermes-webui-brand.css +++ b/dockerfiles/hermes-webui-brand.css @@ -250,3 +250,14 @@ border-radius: 19px; } } + +/* Reserve the device safe-area (home indicator / gesture bar) below the + composer so its control row is never hidden behind it. Requires + viewport-fit=cover on the viewport meta (set in hermes-webui-base-patch.py); + env() resolves to 0 on devices without an inset, so this is inert on desktop. + Additive with the keyboard inset the app already manages via + --keyboard-bottom-inset, and absorbed by the flex-1 message scroller so the + total column height stays within the viewport (body is height:100dvh). */ +.composer-wrap { + padding-bottom: calc(16px + var(--keyboard-bottom-inset, 0px) + env(safe-area-inset-bottom, 0px)) !important; +} diff --git a/testing/tests/test_hermes_webui_brand.py b/testing/tests/test_hermes_webui_brand.py index 7f420600..dde62964 100644 --- a/testing/tests/test_hermes_webui_brand.py +++ b/testing/tests/test_hermes_webui_brand.py @@ -379,6 +379,23 @@ def test_brand_css_is_accessible_dark_and_reduced_motion_aware() -> None: assert "transition: none !important" in reduced assert "transform: none !important" in reduced assert "animation:" not in css + # The composer reserves the device bottom safe-area (home indicator / gesture + # bar) so its control row is not hidden behind it, additive with the app's + # keyboard inset. env() is 0 on inset-less devices, so this is desktop-inert. + assert ".composer-wrap" in css + composer = css.rsplit(".composer-wrap {", 1)[1].split("}", 1)[0] + assert "env(safe-area-inset-bottom, 0px)" in composer + assert "var(--keyboard-bottom-inset, 0px)" in composer + + +def test_base_patch_and_dockerfile_opt_into_edge_to_edge_safe_area() -> None: + """viewport-fit=cover is required for env(safe-area-inset-*) to carry real + values; the base patch adds it and the Dockerfile verifies it landed.""" + base_patch = (DOCKERFILES / "hermes-webui-base-patch.py").read_text(encoding="utf-8") + assert "viewport-fit=cover" in base_patch + assert "user-scalable=no, viewport-fit=cover" in base_patch + dockerfile = (DOCKERFILES / "Dockerfile.hermes-webui").read_text(encoding="utf-8") + assert "grep -Fq 'user-scalable=no, viewport-fit=cover'" in dockerfile def test_empty_state_uses_canonical_character_without_inline_staff(