atlas-iac/dockerfiles/hermes-webui-base-patch.py
jenkins ffa477585d 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvMSXH8VH2tMWXanb8SJdf
2026-08-24 23:32:27 -03:00

97 lines
4.5 KiB
Python

#!/usr/bin/env python3
"""Apply Atlas baseline UI policy patches to the pinned Hermes WebUI."""
from pathlib import Path
config = Path("/opt/hermes-webui/api/config.py")
source = config.read_text(encoding="utf-8")
before = (
'VALID_REASONING_EFFORTS = ("minimal", "low", "medium", "high", "xhigh", "max")'
)
after = 'VALID_REASONING_EFFORTS = ("minimal", "low", "medium", "high", "xhigh")'
if source.count(before) != 1:
raise SystemExit("Hermes WebUI reasoning-effort patch context changed")
config.write_text(source.replace(before, after, 1), encoding="utf-8")
index = Path("/opt/hermes-webui/static/index.html")
source = index.read_text(encoding="utf-8")
before = ' <div class="reasoning-option" data-effort="max">Max</div>\n'
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 = (
'<meta name="viewport" content="width=device-width, initial-scale=1, '
'maximum-scale=1, user-scalable=no">'
)
after = (
'<meta name="viewport" content="width=device-width, initial-scale=1, '
'maximum-scale=1, user-scalable=no, viewport-fit=cover">'
)
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")
source = ui.read_text(encoding="utf-8")
before = """ const res=await fetcher(_offlineHealthUrl(),opts);
return !!(res&&res.ok);
"""
after = """ const res=await fetcher(_offlineHealthUrl(),opts);
if(res&&(res.status===401||res.status===403)){
const rd=window.location.pathname+window.location.search+window.location.hash;
window.location.assign('/oauth2/start?rd='+encodeURIComponent(rd));
return false;
}
return !!(res&&res.ok);
"""
if source.count(before) != 1:
raise SystemExit("Hermes WebUI auth-recovery patch context changed")
ui.write_text(source.replace(before, after, 1), encoding="utf-8")
# Make delegated session hierarchy obvious and collapsible in the sidebar.
sessions = Path("/opt/hermes-webui/static/sessions.js")
source = sessions.read_text(encoding="utf-8")
before = """ const childLabel=t('session_meta_children', childCount);
childCountEl.textContent=childLabel;
childCountEl.title=_sessionChildBadgeTooltip(childLabel);
"""
after = """ const childLabel=t('session_meta_children', childCount);
const childrenExpanded=_expandedChildSessionKeys.has(lineageKey)||!!searchQueryRaw;
childCountEl.textContent=(childrenExpanded?'':'')+childLabel;
childCountEl.setAttribute('aria-expanded',childrenExpanded?'true':'false');
childCountEl.title=_sessionChildBadgeTooltip(childLabel);
"""
if source.count(before) != 1:
raise SystemExit("Hermes WebUI child-session toggle patch context changed")
sessions.write_text(source.replace(before, after, 1), encoding="utf-8")
# A profile's model is only its default; label that scope in both render paths.
panels = Path("/opt/hermes-webui/static/panels.js")
source = panels.read_text(encoding="utf-8")
before = " if (typeof p.model === 'string' && p.model) meta.push(p.model.split('/').pop());\n"
after = """ if (typeof p.model === 'string' && p.model) {
const routeLabels = {
'atlas/auto/fast': 'Automatic · Fast',
'atlas/auto/balanced': 'Automatic · Balanced',
'atlas/auto/deep': 'Automatic · Deep',
'atlas/auto/maximum': 'Automatic · Maximum',
};
meta.push('profile default: ' + (routeLabels[p.model] || p.model.split('/').pop()));
}
"""
if source.count(before) != 2:
raise SystemExit("Hermes WebUI profile-model label patch context changed")
panels.write_text(source.replace(before, after, 2), encoding="utf-8")