Fix premature OVERDUE on the API and the Complete tap doing nothing

Overdue: the API read the host clock (the deployed container runs UTC,
the household is US Eastern), so every morning quest turned overdue
from ~04:00 local, and the snapshot routes built a default settings
object instead of the stored one. Add an additive `timezone` setting
(IANA name; "" keeps the host-local behaviour), validated through the
shared settings field table and exposed on the admin settings GET/PATCH.
Every API clock read (panel/admin routes, the missed-sweep scheduler)
now re-expresses the one host read in that zone, and the snapshot
routes load the persisted settings.

Complete: with target ES2022, TypeScript's useDefineForClassFields
default emitted the cards' reactive fields as native class fields that
shadow Lit's accessors, so setting `_confirm` never re-rendered and the
confirm dialog never opened. Set useDefineForClassFields to false (Lit's
documented TypeScript setting) and rebuild the bundle.

Tests pin the clock at the due-time boundary for both snapshot shapes,
cover the scheduler and on-time stamp, and drive the built bundle's
Complete tap through to the complete_quest service call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-26 09:29:24 -04:00
co-authored by Claude Opus 5.5
parent 8f38946c04
commit 64aa8e99f6
12 changed files with 792 additions and 353 deletions
+63
View File
@@ -828,6 +828,7 @@ def _render_quest_log(
url: str,
idle_ms: int | None = None,
probe_ms: int | None = None,
tap_complete: int | None = None,
) -> dict:
"""Render the built bundle's quest log and report its location.
@@ -851,6 +852,8 @@ def _render_quest_log(
spec["idle_ms"] = idle_ms
if probe_ms is not None:
spec["probe_ms"] = probe_ms
if tap_complete is not None:
spec["tap_complete"] = tap_complete
completed = subprocess.run(
["node", str(QUEST_LOG_HARNESS)],
input=json.dumps(spec),
@@ -952,6 +955,66 @@ def test_quest_complete_screen_returns_to_the_board_after_the_countdown() -> Non
assert result["location"] == "/nestquest"
def test_tapping_complete_confirms_and_calls_the_service() -> None:
"""Tapping an open quest's Complete opens the confirm dialog, and the
dialog's Complete calls nestquest.complete_quest with the panel actor.
With ``useDefineForClassFields`` on (the ES2022 default) the card's
reactive fields were emitted as native class fields that shadowed
Lit's accessors: ``_confirm`` changed but no re-render was scheduled,
so the dialog never opened and the tap did nothing."""
generated = _generate_dashboard(
{}, _three_child_states(), url="http://homeassistant.local/nestquest"
)
open_quest = {
"id": 42,
"definition_id": 7,
"child_id": 1,
"title": "Brush Teeth",
"icon": None,
"window": "morning",
"due_time": "08:15",
"state": "open",
"overdue": True,
"completed_at": None,
"on_time": None,
}
states = {
**_three_child_states(),
"sensor.nestquest_ada_quests_due_today": _state(
"1",
child_name="Ada",
child_id=1,
present=True,
instances=[open_quest],
),
}
result = _render_quest_log(
generated["views"][1]["cards"][0],
states,
url="http://homeassistant.local/nestquest/ada",
tap_complete=open_quest["id"],
)
assert result["tap"]["dialog_opened"] is True
assert result["tap"]["service_calls"] == [
{
"domain": "nestquest",
"service": "complete_quest",
"data": {
"instance_id": open_quest["id"],
"actor": "panel",
"actor_child_id": open_quest["child_id"],
},
}
]
def test_card_build_keeps_lit_reactive_accessors() -> None:
"""The frontend build must not emit class fields that shadow Lit's
reactive property accessors (Lit's documented TypeScript setting)."""
tsconfig = json.loads((FRONTEND_DIR / "tsconfig.json").read_text())
assert tsconfig["compilerOptions"]["useDefineForClassFields"] is False
def test_quest_log_crest_keeps_its_shield_classes() -> None:
"""PANEL-SPEC §3 header crest: a present child's crest renders as the
styled shield, not a bare span. Lit removes an attribute outright when