Skip to content

Commit d223ba9

Browse files
kingpanther13claude
andcommitted
fix(screenshot): re-enable the theme guard around dashboard captures
The ThemeGuard bracket in capture_dashboard_images was commented out on the premise that upstream Puppet had fixed the cold-render settheme dispatch that made it necessary (#1909). That fix never shipped: balloob/home-assistant-addons#89 is merged to that repo's master but unreleased -- it landed after the 2.6.0 version bump (#88), and puppet/config.yaml still reads 2.6.0 with no settheme entry in its CHANGELOG. Every deployment on a released Puppet therefore still has the theme persisted onto the engine token user's profile and synced to that user's live web and mobile sessions, with nothing restoring it. The upstream fix is also narrower than the disable assumed. Its title is exact -- "don't dispatch settheme when no theme/dark was requested" -- so a capture passing theme= or dark_mode=true still writes even once it ships. The bracket is therefore restored unconditionally rather than gated on engine version. ha_get_dashboard_screenshot already documents the restore ("The engine user's saved theme preference is restored after the capture (best effort)"), so the disabled bracket also left the tool description promising behaviour the code did not perform. TestCaptureBracketDisabled asserted the bracket stayed off; it is inverted to TestCaptureBracket, which now asserts the snapshot/restore sessions open and the clobbered theme is written back, including on the capture-failure path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Nm7tyA1nfxNCWFXaR3AxV
1 parent 858d09e commit d223ba9

4 files changed

Lines changed: 63 additions & 66 deletions

File tree

docs/beta.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -177,18 +177,18 @@ token grants. If the token is missing or invalid, Puppet lands on the login
177177
page and (by its design) restarts; ha-mcp surfaces this as a clear "set the
178178
engine's access token" error rather than a silent failure.
179179

180-
Puppet's theme and dark-mode renderer controls used to dispatch Home
181-
Assistant's `settheme` event on every cold render, which Home Assistant
182-
persisted on the frontend profile of the user whose token the engine runs with
183-
— and synced to that user's real web and mobile sessions, flipping a dark-mode
184-
user's whole UI to light on every screenshot (#1909). Recent Puppet versions
185-
fixed that cold-render dispatch, so ha-mcp's snapshot/restore bracket around
186-
each capture is now disabled (#1991); the guard code is retained so it can be
187-
switched back on if a future engine regression reintroduces the write. If you
188-
run an older Puppet build, update the app (or your self-hosted sidecar
189-
image) — older engines still persist the theme selection and will keep
190-
flipping it. A dedicated Puppet account remains a sound belt-and-suspenders
191-
setup. Language selection is local to Puppet's browser session.
180+
Puppet's theme and dark-mode renderer controls dispatch Home Assistant's
181+
`settheme` event on render, which Home Assistant persists on the frontend
182+
profile of the user whose token the engine runs with — and syncs to that
183+
user's real web and mobile sessions, flipping that user's whole UI on every
184+
screenshot (#1909). ha-mcp brackets each capture batch with a snapshot/restore
185+
of that user's saved theme, so the flip is undone automatically. The upstream
186+
Puppet fix for the dispatch (balloob/home-assistant-addons#89) is merged but
187+
unreleased as of Puppet 2.6.0, and is scoped to renders that request no
188+
`theme`/`dark` — an explicit theme or dark-mode capture still writes even once
189+
it ships, so the bracket stays on regardless of engine version. A dedicated
190+
Puppet account remains a sound belt-and-suspenders setup. Language selection
191+
is local to Puppet's browser session.
192192

193193
To change the Puppet engine app's own options (such as `keep_browser_open`)
194194
or to restart it, use `ha_manage_app`; the screenshot tools only render and

src/ha_mcp/dashboard_screenshot/capture.py

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -855,15 +855,9 @@ async def capture_dashboard_images(
855855
preset's native orientation. ``full_page`` is a compatibility alias for
856856
requesting the engine's native ``WIDTHxauto`` viewport.
857857
858-
The batch used to be bracketed by a :class:`ThemeGuard` that restored the
859-
engine user's saved frontend theme when a cold render changed it (issue
860-
#1909). That bracket is currently disabled (#1991): upstream Puppet no
861-
longer dispatches ``settheme`` on cold renders, so there is nothing to undo.
862-
The guard construction and the ``client`` / ``capture_warnings`` plumbing
863-
are retained so the bracket can be re-enabled by uncommenting the
864-
snapshot/restore calls if a future engine regression reintroduces the write;
865-
while disabled ``capture_warnings`` simply stays empty and never affects the
866-
captures themselves.
858+
The batch is bracketed by a :class:`ThemeGuard` that restores the engine
859+
user's saved frontend theme when a render changes it (issue #1909). Guard
860+
failures are non-fatal and surface through ``capture_warnings``.
867861
"""
868862
path = _validate_dashboard_path(dashboard_path)
869863
options = validate_capture_parameters(
@@ -888,16 +882,20 @@ async def capture_dashboard_images(
888882
mime_type = _MIME_TYPES[options.image_format]
889883
captures: list[DashboardImageCapture] = []
890884

891-
# ThemeGuard bracket — currently DISABLED (#1991). Stock Puppet used to
892-
# dispatch a theme write into the authenticated frontend on cold renders,
893-
# which Home Assistant persisted to the engine user's real profile (#1909);
894-
# ha-mcp snapshotted before and restored after to undo it. Upstream Puppet
895-
# has since fixed the cold-render settheme dispatch, so the bracket is no
896-
# longer needed. The guard is still constructed (and the snapshot/restore
897-
# calls kept below, commented out) so it can be re-enabled by uncommenting
898-
# if a future engine regression reintroduces the write.
885+
# ThemeGuard bracket. Puppet dispatches a ``settheme`` event into the
886+
# authenticated frontend, which Home Assistant persists to the engine
887+
# user's real profile and syncs to that user's live sessions (#1909).
888+
# ha-mcp snapshots the saved theme before the batch and restores it after.
889+
#
890+
# The bracket was previously disabled on the premise that upstream Puppet
891+
# had fixed the dispatch. It had not shipped: the fix
892+
# (balloob/home-assistant-addons#89) is merged to that repo's master but
893+
# unreleased — the newest Puppet release, 2.6.0, predates it. It is also
894+
# scoped to the no-parameter case ("don't dispatch settheme when no
895+
# theme/dark was requested"), so an explicit ``theme=``/``dark`` capture
896+
# still writes even once it does ship.
899897
guard = ThemeGuard.for_capture(engine_target.addon_credential, client)
900-
# await guard.take_snapshot()
898+
await guard.take_snapshot()
901899
batch_error: ToolError | None = None
902900
try:
903901
async with httpx.AsyncClient(
@@ -977,7 +975,7 @@ async def capture_dashboard_images(
977975
# and its outcome can be attached to the error payload below.
978976
batch_error = exc
979977
finally:
980-
# await guard.restore() # ThemeGuard bracket disabled (#1991) — see above.
978+
await guard.restore()
981979
if capture_warnings is not None:
982980
capture_warnings.extend(guard.warnings)
983981

src/ha_mcp/dashboard_screenshot/theme_guard.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
"""Snapshot and restore the engine user's saved frontend theme (issue #1909).
22
3-
NOTE: This guard is currently disabled at its call site (``capture.py``)
4-
because upstream Puppet fixed the cold-render ``settheme`` dispatch that made
5-
the bracket necessary (#1991). The code here is retained unchanged so the
6-
bracket can be re-enabled by uncommenting the snapshot/restore calls in
7-
``capture.py`` if a future engine regression reintroduces the write.
8-
9-
Stock Puppet dispatches Home Assistant's ``settheme`` event on every
3+
Puppet dispatches Home Assistant's ``settheme`` event on every
104
cold-browser render — its ``dark`` query flag is presence-based, so "not
115
requested" reaches the frontend as an explicit "light". Home Assistant
126
persists that selection server-side per user (``frontend/set_user_data``,

tests/src/unit/test_dashboard_screenshot_theme_guard.py

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,10 @@
55
flipping that user's real sessions to light mode. The guard snapshots the
66
saved theme before a capture batch and restores it afterwards.
77
8-
The capture-time bracket is currently DISABLED (#1991): upstream Puppet fixed
9-
the cold-render dispatch, so ``capture.py`` no longer calls the guard's
10-
snapshot/restore. The guard code itself is retained (and unchanged), so the
11-
unit tests below still cover its credential resolution and snapshot/restore
12-
semantics against a fake WebSocket client. ``TestCaptureBracketDisabled``
13-
asserts the bracket stays off — capture opens no guard sessions and adds no
14-
warnings — guarding against a silent re-enable.
8+
These cover the guard's credential resolution and snapshot/restore semantics
9+
against a fake WebSocket client, plus ``TestCaptureBracket``, which asserts
10+
``capture.py`` actually brackets each batch — guarding against the bracket
11+
being silently disabled again.
1512
"""
1613

1714
from __future__ import annotations
@@ -366,13 +363,18 @@ async def fake_resolve() -> EngineTarget:
366363
_ClobberingEngineClient.status_code = 200
367364

368365

369-
class TestCaptureBracketDisabled:
370-
"""The ThemeGuard bracket around capture_dashboard_images is DISABLED
371-
(#1991). Upstream Puppet no longer clobbers the theme on cold renders, so
372-
ha-mcp opens no snapshot/restore sessions and adds no guard warnings. These
373-
guard against the bracket being silently re-enabled."""
366+
class TestCaptureBracket:
367+
"""The ThemeGuard bracket around ``capture_dashboard_images`` is ENABLED.
374368
375-
async def test_capture_opens_no_guard_sessions_and_leaves_theme(
369+
Puppet dispatches a ``settheme`` event on render, which Home Assistant
370+
persists onto the engine token user's profile (#1909), so every capture
371+
batch snapshots the saved theme before and restores it after. The bracket
372+
was once disabled on the premise that upstream Puppet had fixed the
373+
dispatch; that fix (balloob/home-assistant-addons#89) is unreleased and is
374+
scoped to the no-parameter case, so the bracket stays on.
375+
"""
376+
377+
async def test_capture_restores_theme_clobbered_by_the_render(
376378
self, monkeypatch: Any
377379
) -> None:
378380
from ha_mcp.dashboard_screenshot import capture
@@ -386,19 +388,19 @@ async def test_capture_opens_no_guard_sessions_and_leaves_theme(
386388
)
387389

388390
assert captures[0].data == _PNG
389-
# No snapshot/restore WebSocket sessions were opened.
390-
assert _FakeWsClient.instances == []
391-
# The bracket did not run: the fake engine still simulates the old
392-
# clobber, and ha-mcp no longer undoes it (real Puppet no longer
393-
# causes it).
394-
assert _FakeWsClient.user_data[THEME_USER_DATA_KEY] == _CLOBBERED_THEME
391+
# Snapshot and restore each opened a session as the engine user.
392+
assert [(ws.url, ws.token) for ws in _FakeWsClient.instances] == [
393+
("http://homeassistant:8123", "puppet-token"),
394+
("http://homeassistant:8123", "puppet-token"),
395+
]
396+
# The fake engine clobbered the stored theme; the bracket undid it.
397+
assert _FakeWsClient.user_data[THEME_USER_DATA_KEY] == _DARK_THEME
395398
assert capture_warnings == []
396399

397-
async def test_client_credential_fallback_opens_no_guard_sessions(
400+
async def test_client_credential_fallback_restores_theme(
398401
self, monkeypatch: Any
399402
) -> None:
400-
"""Sidecar/standalone mode: even with a usable client credential the
401-
disabled bracket opens no guard sessions."""
403+
"""Sidecar/standalone mode: the client credential drives the bracket."""
402404
from ha_mcp.dashboard_screenshot import capture
403405

404406
_patch_engine(monkeypatch, None)
@@ -409,11 +411,14 @@ async def test_client_credential_fallback_opens_no_guard_sessions(
409411
)
410412

411413
assert captures[0].data == _PNG
412-
assert _FakeWsClient.instances == []
414+
assert [(ws.url, ws.token) for ws in _FakeWsClient.instances] == [
415+
("http://ha.local:8123", "own-token"),
416+
("http://ha.local:8123", "own-token"),
417+
]
418+
assert _FakeWsClient.user_data[THEME_USER_DATA_KEY] == _DARK_THEME
413419

414-
async def test_capture_failure_raises_without_guard_warnings(
415-
self, monkeypatch: Any
416-
) -> None:
420+
async def test_capture_failure_still_restores_theme(self, monkeypatch: Any) -> None:
421+
"""A failed render must not leave the engine user's theme clobbered."""
417422
import json
418423

419424
from ha_mcp.dashboard_screenshot import capture
@@ -425,8 +430,8 @@ async def test_capture_failure_raises_without_guard_warnings(
425430
with pytest.raises(ToolError) as exc_info:
426431
await capture.capture_dashboard_images("lovelace/0")
427432

428-
# No guard sessions opened, and no guard warning on the error payload.
429-
assert _FakeWsClient.instances == []
433+
# The restore in the ``finally`` ran before the error surfaced.
434+
assert _FakeWsClient.user_data[THEME_USER_DATA_KEY] == _DARK_THEME
430435
payload = json.loads(str(exc_info.value))
431436
assert not any(
432437
"restoring it failed" in warning for warning in payload.get("warnings", [])

0 commit comments

Comments
 (0)