fix(hermes): hide the worker overlay on dashboard pages
This commit is contained in:
parent
240426d153
commit
2ab737f8f8
@ -25,7 +25,7 @@ spec:
|
|||||||
ai.bstein.dev/execution: Hermes Kanban with durable direct Codex and Claude Code CLI workers
|
ai.bstein.dev/execution: Hermes Kanban with durable direct Codex and Claude Code CLI workers
|
||||||
ai.bstein.dev/model-policy: Jetson-assisted AUTO routing, low through xhigh, cross-provider fallback
|
ai.bstein.dev/model-policy: Jetson-assisted AUTO routing, low through xhigh, cross-provider fallback
|
||||||
ai.bstein.dev/placement: primary amd64 accelerator titan-22; arm64 rpi5 fleet fallback; storage-backbone nodes excluded
|
ai.bstein.dev/placement: primary amd64 accelerator titan-22; arm64 rpi5 fleet fallback; storage-backbone nodes excluded
|
||||||
ai.bstein.dev/config-rev: "20260825-claude-quota-expiry"
|
ai.bstein.dev/config-rev: "20260912-dashboard-bootstrap-visibility"
|
||||||
prometheus.io/scrape: "true"
|
prometheus.io/scrape: "true"
|
||||||
prometheus.io/path: /metrics
|
prometheus.io/path: /metrics
|
||||||
prometheus.io/port: "9010"
|
prometheus.io/port: "9010"
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Merge display-only worker activity into dashboard session transcripts."""
|
"""Serve worker activity and keep its loading overlay scoped to resumed chats."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@ -197,6 +197,20 @@ LATEST_SELECTION_AFTER = ''' children = {}
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
SPA_INDEX_BEFORE = ''' html = _index_path.read_text(encoding="utf-8")
|
||||||
|
'''
|
||||||
|
|
||||||
|
SPA_INDEX_AFTER = SPA_INDEX_BEFORE + ''' # The bundled overlay's inline display:flex overrides HTML hidden.
|
||||||
|
# Honor hidden before React mounts, including ordinary dashboard URLs.
|
||||||
|
html = html.replace(
|
||||||
|
"</head>",
|
||||||
|
"<style>#hermes-resume-bootstrap[hidden]"
|
||||||
|
"{display:none!important}</style></head>",
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
def patch(source: Path, destination: Path) -> None:
|
def patch(source: Path, destination: Path) -> None:
|
||||||
"""Apply the activity projection and fail closed on upstream drift."""
|
"""Apply the activity projection and fail closed on upstream drift."""
|
||||||
content = source.read_text(encoding="utf-8")
|
content = source.read_text(encoding="utf-8")
|
||||||
@ -208,9 +222,12 @@ def patch(source: Path, destination: Path) -> None:
|
|||||||
raise RuntimeError("Hermes dashboard lineage row context changed")
|
raise RuntimeError("Hermes dashboard lineage row context changed")
|
||||||
if LATEST_SELECTION_BEFORE not in content:
|
if LATEST_SELECTION_BEFORE not in content:
|
||||||
raise RuntimeError("Hermes dashboard lineage selection context changed")
|
raise RuntimeError("Hermes dashboard lineage selection context changed")
|
||||||
|
if content.count(SPA_INDEX_BEFORE) != 1:
|
||||||
|
raise RuntimeError("Hermes dashboard bootstrap context changed")
|
||||||
content = content.replace(HELPER_MARKER, HELPER_REPLACEMENT, 1)
|
content = content.replace(HELPER_MARKER, HELPER_REPLACEMENT, 1)
|
||||||
content = content.replace(LATEST_ROWS_BEFORE, LATEST_ROWS_AFTER, 1)
|
content = content.replace(LATEST_ROWS_BEFORE, LATEST_ROWS_AFTER, 1)
|
||||||
content = content.replace(LATEST_SELECTION_BEFORE, LATEST_SELECTION_AFTER, 1)
|
content = content.replace(LATEST_SELECTION_BEFORE, LATEST_SELECTION_AFTER, 1)
|
||||||
|
content = content.replace(SPA_INDEX_BEFORE, SPA_INDEX_AFTER, 1)
|
||||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||||
destination.write_text(
|
destination.write_text(
|
||||||
content.replace(MESSAGES_BEFORE, MESSAGES_AFTER, 1),
|
content.replace(MESSAGES_BEFORE, MESSAGES_AFTER, 1),
|
||||||
|
|||||||
@ -264,6 +264,7 @@ def test_web_session_activity_patch_projects_bounded_events(tmp_path: Path):
|
|||||||
+ module.HELPER_MARKER
|
+ module.HELPER_MARKER
|
||||||
+ " db = object()\n"
|
+ " db = object()\n"
|
||||||
+ module.MESSAGES_BEFORE
|
+ module.MESSAGES_BEFORE
|
||||||
|
+ module.SPA_INDEX_BEFORE
|
||||||
+ "suffix\n",
|
+ "suffix\n",
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
|||||||
118
testing/tests/test_hermes_dashboard_bootstrap.py
Normal file
118
testing/tests/test_hermes_dashboard_bootstrap.py
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
"""Regression checks for the worker dashboard's first-paint loading overlay."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import os
|
||||||
|
import textwrap
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
SPEC = importlib.util.spec_from_file_location(
|
||||||
|
"dashboard_activity_patch",
|
||||||
|
ROOT / "services/hermes/scripts/patch_web_session_activity.py",
|
||||||
|
)
|
||||||
|
assert SPEC and SPEC.loader
|
||||||
|
PATCH = importlib.util.module_from_spec(SPEC)
|
||||||
|
SPEC.loader.exec_module(PATCH)
|
||||||
|
|
||||||
|
|
||||||
|
def _backend_source() -> str:
|
||||||
|
"""Provide the upstream anchors required by the deployment's patcher."""
|
||||||
|
return "\n".join(
|
||||||
|
(
|
||||||
|
PATCH.HELPER_MARKER,
|
||||||
|
PATCH.MESSAGES_BEFORE,
|
||||||
|
PATCH.LATEST_ROWS_BEFORE,
|
||||||
|
PATCH.LATEST_SELECTION_BEFORE,
|
||||||
|
PATCH.SPA_INDEX_BEFORE,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("occurrences", [0, 2])
|
||||||
|
def test_bootstrap_patch_rejects_upstream_drift(tmp_path, occurrences):
|
||||||
|
"""An ambiguous HTML insertion point must fail before writing a patch."""
|
||||||
|
source = tmp_path / "web_server.py"
|
||||||
|
destination = tmp_path / "patched.py"
|
||||||
|
source.write_text(
|
||||||
|
_backend_source().replace(
|
||||||
|
PATCH.SPA_INDEX_BEFORE, PATCH.SPA_INDEX_BEFORE * occurrences
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
with pytest.raises(RuntimeError, match="bootstrap context changed"):
|
||||||
|
PATCH.patch(source, destination)
|
||||||
|
assert not destination.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def _render_bootstrap(tmp_path: Path) -> str:
|
||||||
|
"""Run the patched HTML handler against the image's actual bootstrap."""
|
||||||
|
source = tmp_path / "web_server.py"
|
||||||
|
destination = tmp_path / "patched.py"
|
||||||
|
source.write_text(_backend_source(), encoding="utf-8")
|
||||||
|
PATCH.patch(source, destination)
|
||||||
|
patched = destination.read_text(encoding="utf-8")
|
||||||
|
html_handler = patched[patched.index(PATCH.SPA_INDEX_BEFORE):]
|
||||||
|
namespace = {}
|
||||||
|
exec(
|
||||||
|
"def serve(_index_path):\n"
|
||||||
|
+ textwrap.indent(textwrap.dedent(html_handler), " ")
|
||||||
|
+ " return html\n",
|
||||||
|
namespace,
|
||||||
|
)
|
||||||
|
dockerfile = (ROOT / "dockerfiles/Dockerfile.hermes-agent").read_text()
|
||||||
|
body = dockerfile.split("` <body>\n", 1)[1].split(
|
||||||
|
'<div id="root"></div>`', 1
|
||||||
|
)[0]
|
||||||
|
index = tmp_path / "index.html"
|
||||||
|
index.write_text(
|
||||||
|
"<!doctype html><html><head></head><body>"
|
||||||
|
+ body
|
||||||
|
+ '<div id="root">Dashboard</div></body></html>',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
return namespace["serve"](index)
|
||||||
|
|
||||||
|
|
||||||
|
def test_bootstrap_patch_preserves_the_page_and_resume_script(tmp_path):
|
||||||
|
"""HTML serving retains the app mount and the existing resume behavior."""
|
||||||
|
html = _render_bootstrap(tmp_path)
|
||||||
|
assert '<div id="root">Dashboard</div>' in html
|
||||||
|
assert 'loading.hidden = false' in html
|
||||||
|
assert html.count("<head>") == html.count("</head>") == 1
|
||||||
|
assert html.count("<style>") == html.count("</style>") == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("engine", ["chromium", "firefox"])
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("route", "visible"),
|
||||||
|
[("/", False), ("/chat", False), ("/sessions?resume=worker", False),
|
||||||
|
("/chat?resume=worker", True)],
|
||||||
|
)
|
||||||
|
def test_bootstrap_visibility_without_the_bundle(tmp_path, engine, route, visible):
|
||||||
|
"""A failed or delayed bundle cannot cover ordinary dashboard routes."""
|
||||||
|
playwright = pytest.importorskip("playwright.sync_api")
|
||||||
|
html = _render_bootstrap(tmp_path)
|
||||||
|
with playwright.sync_playwright() as runtime:
|
||||||
|
browser = getattr(runtime, engine).launch(
|
||||||
|
executable_path=os.environ.get(f"HERMES_TEST_{engine.upper()}_EXECUTABLE"),
|
||||||
|
headless=True,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
page = browser.new_page()
|
||||||
|
page.route("**/*", lambda request: request.fulfill(
|
||||||
|
content_type="text/html", body=html
|
||||||
|
))
|
||||||
|
page.goto("http://hermes.test" + route)
|
||||||
|
overlay = page.locator("#hermes-resume-bootstrap")
|
||||||
|
assert overlay.is_visible() is visible
|
||||||
|
if visible:
|
||||||
|
# SessionActivityPanel removes the overlay when it mounts.
|
||||||
|
overlay.evaluate("element => element.remove()")
|
||||||
|
assert overlay.count() == 0
|
||||||
|
finally:
|
||||||
|
browser.close()
|
||||||
Loading…
x
Reference in New Issue
Block a user