From ffa477585d225f99a82b4049aa461a66a5de4189 Mon Sep 17 00:00:00 2001 From: jenkins Date: Mon, 24 Aug 2026 23:32:26 -0300 Subject: [PATCH] hermes(webui): fix mobile composer hidden behind the gesture bar On an Android standalone PWA the composer's bottom control row was rendering about one line below the visible viewport, behind the system gesture bar, so those controls were unreachable. The theme already pads the titlebar with env(safe-area-inset-top/left/right), but the viewport meta never opted into viewport-fit=cover, so every safe-area inset collapsed to 0 and the bottom edge had no reservation. Add viewport-fit=cover to the viewport meta (base patch) so the insets carry real values, and reserve env(safe-area-inset-bottom) at the bottom of the composer (brand.css), additive with the app's existing --keyboard-bottom-inset and absorbed by the flex-1 message scroller so total height stays within the viewport. Inert on desktop (env() resolves to 0). Dockerfile verifies the meta patch landed; brand/dockerfile contracts covered by tests. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01BvMSXH8VH2tMWXanb8SJdf --- dockerfiles/Dockerfile.hermes-webui | 1 + dockerfiles/hermes-webui-base-patch.py | 20 ++++++++++++++++++++ dockerfiles/hermes-webui-brand.css | 11 +++++++++++ testing/tests/test_hermes_webui_brand.py | 17 +++++++++++++++++ 4 files changed, 49 insertions(+) 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(