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
196 lines
13 KiB
Docker
196 lines
13 KiB
Docker
# syntax=docker/dockerfile:1
|
|
# dockerfiles/Dockerfile.hermes-webui
|
|
FROM ghcr.io/nesquena/hermes-webui@sha256:a83a3893111dcb250e7aa7aa657d3d6f4570b0e2fd00d9b7569246fc5e7339b2 AS webui
|
|
|
|
FROM registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
|
|
|
|
ARG HERMES_WEBUI_RELEASE_ID
|
|
|
|
USER root
|
|
|
|
# Keep WebUI and Hermes pinned together. The WebUI imports Hermes internals,
|
|
# while the gateway remains the only process that owns an agent conversation.
|
|
COPY --from=webui /apptoo /opt/hermes-webui
|
|
COPY dockerfiles/hermes-hux-foundation/hux /opt/hermes-hux/hux
|
|
COPY dockerfiles/hermes-hux-foundation/hux_producer /opt/hermes-hux/hux_producer
|
|
COPY services/hermes/contracts/hux /opt/hermes-hux/contracts
|
|
ENV HUX_CONTRACT_DIR=/opt/hermes-hux/contracts
|
|
|
|
# Add the Atlas voice bridge as a narrow integration layer. It activates only
|
|
# when a tenant's server-side STT capability reports the private Jetson route.
|
|
COPY dockerfiles/hermes-webui-base-patch.py /tmp/hermes-webui-base-patch.py
|
|
COPY dockerfiles/hermes-webui-atlas-patch.py /tmp/hermes-webui-atlas-patch.py
|
|
COPY dockerfiles/hermes-webui-stt-patch.py /tmp/hermes-webui-stt-patch.py
|
|
COPY dockerfiles/hermes-webui-telegram-project-patch.py /tmp/hermes-webui-telegram-project-patch.py
|
|
COPY dockerfiles/hermes-webui-atlas-voice.js /opt/hermes-webui/static/atlas-voice.js
|
|
COPY dockerfiles/hermes-webui-atlas-voice-worklet.js /opt/hermes-webui/static/atlas-voice-worklet.js
|
|
COPY dockerfiles/hermes-webui-atlas-voice.css /opt/hermes-webui/static/atlas-voice.css
|
|
COPY dockerfiles/hermes-webui-router-patch.py /tmp/hermes-webui-router-patch.py
|
|
COPY dockerfiles/hermes-webui-router.js /opt/hermes-webui/static/atlas-router.js
|
|
COPY dockerfiles/hermes-webui-hux-bff-patch.py /tmp/hermes-webui-hux-bff-patch.py
|
|
COPY dockerfiles/hermes-webui-hux-context.py /tmp/hermes-webui-hux-context.py
|
|
COPY dockerfiles/hermes-webui-hux-context-patch.py /tmp/hermes-webui-hux-context-patch.py
|
|
COPY dockerfiles/hermes-webui-hux-patch.py /tmp/hermes-webui-hux-patch.py
|
|
COPY dockerfiles/hermes-webui-hux/foundation.css /opt/hermes-webui/static/hux/foundation.css
|
|
COPY dockerfiles/hermes-webui-hux/foundation.js /opt/hermes-webui/static/hux/foundation.js
|
|
COPY dockerfiles/hermes-webui-hux/shell.js /opt/hermes-webui/static/hux/shell.js
|
|
COPY dockerfiles/hermes-webui-hux/runtime/autonomy-privacy.css /opt/hermes-webui/static/hux/runtime/autonomy-privacy.css
|
|
COPY dockerfiles/hermes-webui-hux/runtime/autonomy-privacy.js /opt/hermes-webui/static/hux/runtime/autonomy-privacy.js
|
|
COPY dockerfiles/hermes-webui-hux/runtime/wave_a_activity_memory.css /opt/hermes-webui/static/hux/runtime/wave_a_activity_memory.css
|
|
COPY dockerfiles/hermes-webui-hux/runtime/wave_a_activity_memory.js /opt/hermes-webui/static/hux/runtime/wave_a_activity_memory.js
|
|
COPY dockerfiles/hermes-webui-hux/runtime/wave_a_contract.js /opt/hermes-webui/static/hux/runtime/wave_a_contract.js
|
|
COPY dockerfiles/hermes-webui-hux/runtime/wave_b.css /opt/hermes-webui/static/hux/runtime/wave_b.css
|
|
COPY dockerfiles/hermes-webui-hux/runtime/wave_b_artifacts_research.js /opt/hermes-webui/static/hux/runtime/wave_b_artifacts_research.js
|
|
COPY dockerfiles/hermes-webui-hux/runtime/wave_b_contract.js /opt/hermes-webui/static/hux/runtime/wave_b_contract.js
|
|
COPY dockerfiles/hermes-webui-hux/runtime/wave_b_projects_modes.js /opt/hermes-webui/static/hux/runtime/wave_b_projects_modes.js
|
|
COPY dockerfiles/hermes-webui-hux/runtime/wave_b_runtime.js /opt/hermes-webui/static/hux/runtime/wave_b_runtime.js
|
|
COPY dockerfiles/hermes-webui-hux/runtime/wave_c_multimodal_onboarding_release.css /opt/hermes-webui/static/hux/runtime/wave_c_multimodal_onboarding_release.css
|
|
COPY dockerfiles/hermes-webui-hux/runtime/wave_c_multimodal_onboarding_release.js /opt/hermes-webui/static/hux/runtime/wave_c_multimodal_onboarding_release.js
|
|
COPY dockerfiles/hermes-webui-hux/bootstrap.css /opt/hermes-webui/static/hux/bootstrap.css
|
|
COPY dockerfiles/hermes-webui-hux/bootstrap.js /opt/hermes-webui/static/hux/bootstrap.js
|
|
COPY dockerfiles/hermes-webui-brand-patch.py /tmp/hermes-webui-brand-patch.py
|
|
COPY dockerfiles/hermes-webui-manifest-patch.py /tmp/hermes-webui-manifest-patch.py
|
|
COPY dockerfiles/hermes-webui-release-patch.py /tmp/hermes-webui-release-patch.py
|
|
COPY dockerfiles/hermes-webui-smoke.py /tmp/hermes-webui-smoke.py
|
|
COPY dockerfiles/hermes-webui-brand.css /opt/hermes-webui/static/hermes-brand.css
|
|
COPY dockerfiles/hermes-webui-manifest.json /tmp/hermes-webui-manifest.json
|
|
COPY dockerfiles/hermes-webui-assets/hermes-agent.ico /opt/hermes-webui/static/hermes-agent.ico
|
|
COPY dockerfiles/hermes-webui-assets/hermes-agent-192.png /opt/hermes-webui/static/hermes-agent-192.png
|
|
COPY dockerfiles/hermes-webui-assets/hermes-agent-512.png /opt/hermes-webui/static/hermes-agent-512.png
|
|
RUN /opt/hermes/.venv/bin/python /tmp/hermes-webui-base-patch.py \
|
|
&& /opt/hermes/.venv/bin/python /tmp/hermes-webui-atlas-patch.py \
|
|
&& /opt/hermes/.venv/bin/python /tmp/hermes-webui-stt-patch.py \
|
|
&& /opt/hermes/.venv/bin/python /tmp/hermes-webui-telegram-project-patch.py \
|
|
&& /opt/hermes/.venv/bin/python /tmp/hermes-webui-router-patch.py \
|
|
&& /opt/hermes/.venv/bin/python /tmp/hermes-webui-hux-bff-patch.py \
|
|
&& /opt/hermes/.venv/bin/python /tmp/hermes-webui-hux-context-patch.py \
|
|
&& /opt/hermes/.venv/bin/python /tmp/hermes-webui-brand-patch.py \
|
|
&& /opt/hermes/.venv/bin/python /tmp/hermes-webui-manifest-patch.py \
|
|
&& /opt/hermes/.venv/bin/python /tmp/hermes-webui-hux-patch.py \
|
|
&& HERMES_WEBUI_RELEASE_ID="${HERMES_WEBUI_RELEASE_ID}" \
|
|
/opt/hermes/.venv/bin/python /tmp/hermes-webui-release-patch.py
|
|
|
|
RUN /opt/hermes/.venv/bin/python -c 'import cryptography, yaml' \
|
|
&& test -f /opt/hermes-hux/contracts/flags.json \
|
|
&& test -f /opt/hermes-hux/contracts/release-ledger.schema.json \
|
|
&& PYTHONPATH=/opt/hermes-hux /opt/hermes/.venv/bin/python -m compileall -q /opt/hermes-hux/hux /opt/hermes-hux/hux_producer \
|
|
&& PYTHONPATH=/opt/hermes-hux /opt/hermes/.venv/bin/python -c 'from pathlib import Path; from tempfile import TemporaryDirectory; from hux.server import build_router; root = TemporaryDirectory(); router = build_router(Path(root.name), {"HUX_FLAGS": ""}); assert router.routes; root.cleanup()' \
|
|
&& grep -Fq 'VALID_REASONING_EFFORTS = ("minimal", "low", "medium", "high", "xhigh")' \
|
|
/opt/hermes-webui/api/config.py \
|
|
&& ! grep -Fq 'data-effort="max"' /opt/hermes-webui/static/index.html \
|
|
&& grep -Fq 'res.status===401||res.status===403' /opt/hermes-webui/static/ui.js \
|
|
&& grep -Fq "window.location.assign('/oauth2/start?rd='" /opt/hermes-webui/static/ui.js \
|
|
&& grep -Fq "childrenExpanded?'▾ ':'▸ '" /opt/hermes-webui/static/sessions.js \
|
|
&& 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 \
|
|
&& grep -Fq 'Audio conversion failed: upload is invalid' /opt/hermes/tools/transcription_tools.py \
|
|
&& grep -Fq "capability.provider!=='local_command'" /opt/hermes-webui/static/atlas-voice.js \
|
|
&& grep -Fq 'prefers-reduced-motion: reduce' /opt/hermes-webui/static/atlas-voice.css \
|
|
&& grep -Fq 'id="voiceInstrumentStyles"' /opt/hermes-webui/static/index.html \
|
|
&& grep -Fq 'data-priority="maximum"' /opt/hermes-webui/static/index.html \
|
|
&& grep -Fq 'routing_priority:priority' /opt/hermes-webui/static/atlas-router.js \
|
|
&& grep -Fq "'atlas/auto/fast':'Automatic · Fast'" /opt/hermes-webui/static/atlas-router.js \
|
|
&& grep -Fq 'explicit_reasoning_effort' /opt/hermes-webui/api/gateway_chat.py \
|
|
&& grep -Fq 'from api.hux_bff import proxy_hux' /opt/hermes-webui/api/routes.py \
|
|
&& grep -Fq 'from api.hux_context import attach_hux_context' /opt/hermes-webui/api/routes.py \
|
|
&& grep -Fq '"schema": "hux.webui_context.v1"' /opt/hermes-webui/api/hux_context.py \
|
|
&& grep -Fq '"project_source": raw_project' /opt/hermes-webui/api/hux_context.py \
|
|
&& grep -Fq 'static/hux/bootstrap.js' /opt/hermes-webui/static/index.html \
|
|
&& grep -Fq "'./static/hux/bootstrap.js' + VQ" /opt/hermes-webui/static/sw.js \
|
|
&& grep -Fq 'value.hux_context' /opt/hermes-webui/static/hux/bootstrap.js \
|
|
&& grep -Fq "'/hux/v1/capabilities'" /opt/hermes-webui/static/hux/bootstrap.js \
|
|
&& grep -Fq "'/hux/v1/context/bootstrap'" /opt/hermes-webui/static/hux/bootstrap.js \
|
|
&& grep -Fq 'HermesHuxBootstrap.mergeTrustedContext(S.session, startData)' /opt/hermes-webui/static/messages.js \
|
|
&& grep -Fq 'respBody.cancelled===true && respBody.stream_id===streamId' /opt/hermes-webui/static/boot.js \
|
|
&& grep -Fq 'prefers-reduced-motion: no-preference' /opt/hermes-webui/static/hux/bootstrap.css \
|
|
&& grep -Fq '"language": detected_language' /opt/hermes/tools/transcription_tools.py \
|
|
&& grep -Fq "'language': detected" /opt/hermes-webui/api/upload.py \
|
|
&& grep -Fq 'def _atlas_tts_language(body):' /opt/hermes-webui/api/routes.py \
|
|
&& grep -Fq 'request_payload["language"] = _atlas_language' /opt/hermes-webui/api/routes.py \
|
|
&& grep -Fq 'takeSttLanguage(token)' /opt/hermes-webui/static/atlas-voice.js \
|
|
&& grep -Fq "registerProcessor('atlas-pcm-playback'" /opt/hermes-webui/static/atlas-voice-worklet.js \
|
|
&& grep -Fq "const TTS_STREAM_URL='/api/tts/stream'" /opt/hermes-webui/static/atlas-voice.js \
|
|
&& grep -Fq "const STT_STREAM_PATH='/api/transcribe/stream'" /opt/hermes-webui/static/atlas-voice.js \
|
|
&& grep -Fq '<title>Hermes Chat</title>' /opt/hermes-webui/static/index.html \
|
|
&& grep -Fq 'id="hermesBrandStyles"' /opt/hermes-webui/static/index.html \
|
|
&& grep -Fq 'static/hermes-agent-512.png' /opt/hermes-webui/static/index.html \
|
|
&& grep -Fq 'prefers-reduced-motion: reduce' /opt/hermes-webui/static/hermes-brand.css \
|
|
&& grep -Fq '"name": "Hermes Chat"' /opt/hermes-webui/static/manifest.json \
|
|
&& grep -Fq "'./static/hermes-agent-512.png'" /opt/hermes-webui/static/sw.js \
|
|
&& printf '%s %s\n' \
|
|
'aefe65e6574e6f46d3382588f46c508e1b3f3b3c9ce3dec6d335403a5374add9' \
|
|
'/opt/hermes-webui/static/hermes-agent.ico' \
|
|
| sha256sum -c - \
|
|
&& printf '%s %s\n' \
|
|
'0e4102cc715372058dd6ab55e9cde2567e46fee5ab6557b564cdd212ccd2616f' \
|
|
'/opt/hermes-webui/static/hermes-agent-192.png' \
|
|
| sha256sum -c - \
|
|
&& printf '%s %s\n' \
|
|
'6661e5ca0ecc690af213b9f85f961541e6d3d0e36946ede9d3c2c97dfbd3c23d' \
|
|
'/opt/hermes-webui/static/hermes-agent-512.png' \
|
|
| sha256sum -c - \
|
|
&& /opt/hermes/.venv/bin/python -m py_compile \
|
|
/opt/hermes-webui/api/routes.py \
|
|
/opt/hermes-webui/api/upload.py \
|
|
/opt/hermes-webui/api/gateway_chat.py \
|
|
/opt/hermes-webui/api/hux_bff.py \
|
|
/opt/hermes-webui/api/hux_context.py \
|
|
/opt/hermes/tools/transcription_tools.py
|
|
|
|
# Exercise branded responses from the real upstream server process in the
|
|
# target architecture before publish. The manifest checks resolve icon paths
|
|
# from the URL the server actually returns; no filesystem-only assertion can
|
|
# satisfy this gate.
|
|
RUN set -eu; \
|
|
mkdir -p /tmp/hermes-webui-smoke/home /tmp/hermes-webui-smoke/state /tmp/hermes-webui-smoke/workspace; \
|
|
HERMES_HOME=/tmp/hermes-webui-smoke/home \
|
|
HOME=/tmp/hermes-webui-smoke/home \
|
|
HERMES_WEBUI_STATE_DIR=/tmp/hermes-webui-smoke/state \
|
|
HERMES_WEBUI_DEFAULT_WORKSPACE=/tmp/hermes-webui-smoke/workspace \
|
|
HERMES_WEBUI_HOST=127.0.0.1 \
|
|
HERMES_WEBUI_PORT=18787 \
|
|
HERMES_WEBUI_SKIP_ONBOARDING=1 \
|
|
/opt/hermes/.venv/bin/python /opt/hermes-webui/server.py >/tmp/hermes-webui-smoke.log 2>&1 & \
|
|
server_pid=$!; \
|
|
cleanup() { kill "${server_pid}" 2>/dev/null || true; wait "${server_pid}" 2>/dev/null || true; }; \
|
|
trap cleanup EXIT HUP INT TERM; \
|
|
ready=0; \
|
|
for attempt in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30; do \
|
|
if /opt/hermes/.venv/bin/python -c 'from urllib.request import urlopen; urlopen("http://127.0.0.1:18787/health", timeout=2).read()' >/dev/null 2>&1; then ready=1; break; fi; \
|
|
sleep 1; \
|
|
done; \
|
|
smoke_passed=0; \
|
|
if [ "${ready}" = "1" ] \
|
|
&& /opt/hermes/.venv/bin/python /tmp/hermes-webui-smoke.py http://127.0.0.1:18787/; then \
|
|
smoke_passed=1; \
|
|
fi; \
|
|
cleanup; \
|
|
trap - EXIT HUP INT TERM; \
|
|
if [ "${ready}" != "1" ] || [ "${smoke_passed}" != "1" ]; then \
|
|
cat /tmp/hermes-webui-smoke.log; \
|
|
exit 1; \
|
|
fi; \
|
|
rm -rf /tmp/hermes-webui-smoke /tmp/hermes-webui-smoke.log
|
|
|
|
ENV HERMES_WEBUI_AGENT_DIR=/opt/hermes \
|
|
HERMES_WEBUI_HOST=0.0.0.0 \
|
|
HERMES_WEBUI_PORT=8787 \
|
|
HERMES_WEBUI_CHAT_BACKEND=gateway \
|
|
HERMES_WEBUI_GATEWAY_BASE_URL=http://127.0.0.1:8642 \
|
|
HERMES_WEBUI_GATEWAY_USE_RUNS_API=true \
|
|
HERMES_WEBUI_SKIP_ONBOARDING=1 \
|
|
HERMES_WEBUI_SECURE=1 \
|
|
PYTHONPATH=/opt/hermes-hux \
|
|
PYTHONDONTWRITEBYTECODE=1 \
|
|
PYTHONUNBUFFERED=1
|
|
|
|
WORKDIR /opt/hermes-webui
|
|
USER 10000:10000
|
|
EXPOSE 8787
|
|
ENTRYPOINT ["/opt/hermes/.venv/bin/python", "/opt/hermes-webui/server.py"]
|