Maestro task 6351b36d-0eef-4e7d-bf5a-96b754cbc883 — "Time zone errors"
Done condition: "Tasks will not show overdue and all items will be based on the local time zone. Currently, it seems all times are based on utc."
Root cause
Kids' panel cards → Home Assistant entities → coordinator → GET /api/v1/panel/snapshot. The API's _household_clock() returns settings.household_now(_local_now()). When the household timezone setting is empty (as it is in the deployed household), household_now hands back the host-local instant unchanged, and the deployed host runs UTC. snapshot.py then evaluates now.time() > due_moment against UTC, so a 10:00 quest shows overdue from 06:00 Eastern, and today_iso rolls over at UTC midnight. The only thing that ever filled in the zone automatically was the admin web app's browser auto-set, which runs only when an admin opens it.
Fix
The integration now sends hass.config.time_zone in an X-NestQuest-Timezone header on every panel request (api_client.py, coordinator.py).
The panel routes pass that header to the new core settings_store.adopt_reported_timezone(). While timezone_configured is false, the core saves the reported zone and leaves timezone_configured false, since the value is automatic rather than an admin choice. The check and the write happen under the settings lock.
Because the zone is saved, the admin plane also reads the household clock. Later changes to the zone in Home Assistant carry over too.
Scheduler re-planning (review finding TZ-001): the missed-sweep scheduler used to plan its next sweep once and then sleep on those settings. So if a zone was adopted after start-up, the old plan stayed in place and a UTC host swept at UTC midnight (20:00 in New York). Two changes fix this:
After adopting a zone, the panel route calls the new MissedSweepScheduler.settings_changed(settings). If the stored zone or rollover time differs from the scheduler's current plan, this wakes it so it plans again. It is an event, not a poll.
Before sweeping, the loop re-reads the settings. If they no longer match its plan, it plans again instead of sweeping.
Adoption serialised with the scheduler (review finding TZ-003, cf118cf): a new MissedSweepScheduler.settings_lock is held by the panel adoption across its write and its settings_changed call. The loop holds the same lock across (a) its plan read and (b) its pre-sweep re-read plus the sweep. So an adoption commits before a plan read (which sees it), after one (the wake compares against that exact plan), or after the sweep. It never lands between a read and what the loop does with it. The lock is never held across the wait, and the loop holds nothing else while acquiring it, so a panel request cannot deadlock; at most it waits for one settings read or one sweep. The guarantee is documented in the module docstring.
Wait cleanup (review finding TZ-004, cf118cf):_sleep_unless_woken now cancels and awaits both child tasks, and re-raises a failed sleep inside the loop's retry block. A failing sleep backs off and re-plans instead of sweeping, and nothing outlives stop().
An explicit admin choice, including an explicitly chosen empty zone, still wins.
An invalid reported zone is logged and ignored.
No schema change, no migration, and the one-clock-read-at-the-edge rule is unchanged.
Tests from the original change fail on base 07f2eec and pass here:
zone left unset, host clock on UTC:
09:59:59 EDT is not overdue (checked through both the panel and the admin snapshots)
23:30 EDT still reports today's today_iso
a 09:30 tap counts as on time
an automatically adopted zone follows a later change in Home Assistant
the header is sent, and both client factories pass hass.config.time_zone
Tests added for the review findings (each checked in a temporary worktree):
TZ-001 (both fail on 09d6a55, pass here):
the scheduler starts with no zone and plans for UTC midnight. A panel request then adopts New York while the scheduler sleeps, and it plans again for New York midnight: delays of 11 hours, then 15 hours.
the zone is saved without a wake-up. When the stale rollover arrives, the scheduler runs no sweep and plans again for New York midnight.
TZ-002 (fail on base 07f2eec, pass here):
the admin-precedence tests ("America/Los_Angeles" and "") first prove automatic adoption, then apply the admin choice and a conflicting reported zone.
the invalid-header test first adopts New York, then sends an invalid header. The adopted zone stays in place and 09:59:59 EDT is still not overdue.
new client test test_stream_events_reports_household_timezone: the server-sent events request sends both the Authorization and the X-NestQuest-Timezone headers.
TZ-003 / TZ-004 (all four fail on bcf387d, pass here; the new tests were run 15 times with no flakes):
test_scheduler_follows_successive_zone_changes: New York → UTC → New York, with a real panel adoption issued while the re-plan read is in flight. The scheduler ends up sleeping on New York midnight (delays 15h, 11h, 15h). On bcf387d it stays asleep on UTC.
test_adoption_after_presweep_check_cannot_precede_the_sweep: a panel adoption is issued right after the pre-sweep read. The sweep only ever runs with the zone it planned for still stored, then the scheduler re-plans for New York. On bcf387d the UTC sweep ran after New York had been stored.
test_failed_sleep_is_retried_not_swept: a sleep that raises backs off 60s and re-plans; nothing is swept. On bcf387d it swept.
test_stop_waits_for_the_sleep_to_finish_cancelling: after stop() the sleep task is done and its cancellation cleanup has run. On bcf387d it was still pending.
Deploy note
Both the API and the Home Assistant integration need upgrading. The fix takes effect on the integration's first panel request after the upgrade.
Maestro task `6351b36d-0eef-4e7d-bf5a-96b754cbc883` — "Time zone errors"
**Done condition:** "Tasks will not show overdue and all items will be based on the local time zone. Currently, it seems all times are based on utc."
## Root cause
Kids' panel cards → Home Assistant entities → coordinator → `GET /api/v1/panel/snapshot`. The API's `_household_clock()` returns `settings.household_now(_local_now())`. When the household `timezone` setting is empty (as it is in the deployed household), `household_now` hands back the host-local instant unchanged, and the deployed host runs UTC. `snapshot.py` then evaluates `now.time() > due_moment` against UTC, so a 10:00 quest shows overdue from 06:00 Eastern, and `today_iso` rolls over at UTC midnight. The only thing that ever filled in the zone automatically was the admin web app's browser auto-set, which runs only when an admin opens it.
## Fix
- The integration now sends `hass.config.time_zone` in an `X-NestQuest-Timezone` header on every panel request (`api_client.py`, `coordinator.py`).
- The panel routes pass that header to the new core `settings_store.adopt_reported_timezone()`. While `timezone_configured` is false, the core saves the reported zone and leaves `timezone_configured` false, since the value is automatic rather than an admin choice. The check and the write happen under the settings lock.
- Because the zone is saved, the admin plane also reads the household clock. Later changes to the zone in Home Assistant carry over too.
- **Scheduler re-planning (review finding TZ-001):** the missed-sweep scheduler used to plan its next sweep once and then sleep on those settings. So if a zone was adopted after start-up, the old plan stayed in place and a UTC host swept at UTC midnight (20:00 in New York). Two changes fix this:
- After adopting a zone, the panel route calls the new `MissedSweepScheduler.settings_changed(settings)`. If the stored zone or rollover time differs from the scheduler's current plan, this wakes it so it plans again. It is an event, not a poll.
- Before sweeping, the loop re-reads the settings. If they no longer match its plan, it plans again instead of sweeping.
- **Adoption serialised with the scheduler (review finding TZ-003, `cf118cf`):** a new `MissedSweepScheduler.settings_lock` is held by the panel adoption across its write and its `settings_changed` call. The loop holds the same lock across (a) its plan read and (b) its pre-sweep re-read plus the sweep. So an adoption commits before a plan read (which sees it), after one (the wake compares against that exact plan), or after the sweep. It never lands between a read and what the loop does with it. The lock is never held across the wait, and the loop holds nothing else while acquiring it, so a panel request cannot deadlock; at most it waits for one settings read or one sweep. The guarantee is documented in the module docstring.
- **Wait cleanup (review finding TZ-004, `cf118cf`):** `_sleep_unless_woken` now cancels *and awaits* both child tasks, and re-raises a failed sleep inside the loop's retry block. A failing sleep backs off and re-plans instead of sweeping, and nothing outlives `stop()`.
- An explicit admin choice, including an explicitly chosen empty zone, still wins.
- An invalid reported zone is logged and ignored.
- No schema change, no migration, and the one-clock-read-at-the-edge rule is unchanged.
## Tests
- `TZ=UTC uv run pytest -q` → **2168 passed, 1 skipped** (head `cf118cf`)
- Tests from the original change fail on base `07f2eec` and pass here:
- zone left unset, host clock on UTC:
- 09:59:59 EDT is not overdue (checked through both the panel and the admin snapshots)
- 23:30 EDT still reports today's `today_iso`
- a 09:30 tap counts as on time
- an automatically adopted zone follows a later change in Home Assistant
- the header is sent, and both client factories pass `hass.config.time_zone`
- Tests added for the review findings (each checked in a temporary worktree):
- TZ-001 (both fail on `09d6a55`, pass here):
- the scheduler starts with no zone and plans for UTC midnight. A panel request then adopts New York while the scheduler sleeps, and it plans again for New York midnight: delays of 11 hours, then 15 hours.
- the zone is saved without a wake-up. When the stale rollover arrives, the scheduler runs no sweep and plans again for New York midnight.
- TZ-002 (fail on base `07f2eec`, pass here):
- the admin-precedence tests (`"America/Los_Angeles"` and `""`) first prove automatic adoption, then apply the admin choice and a conflicting reported zone.
- the invalid-header test first adopts New York, then sends an invalid header. The adopted zone stays in place and 09:59:59 EDT is still not overdue.
- new client test `test_stream_events_reports_household_timezone`: the server-sent events request sends both the Authorization and the `X-NestQuest-Timezone` headers.
- TZ-003 / TZ-004 (all four fail on `bcf387d`, pass here; the new tests were run 15 times with no flakes):
- `test_scheduler_follows_successive_zone_changes`: New York → UTC → New York, with a real panel adoption issued while the re-plan read is in flight. The scheduler ends up sleeping on New York midnight (delays 15h, 11h, 15h). On `bcf387d` it stays asleep on UTC.
- `test_adoption_after_presweep_check_cannot_precede_the_sweep`: a panel adoption is issued right after the pre-sweep read. The sweep only ever runs with the zone it planned for still stored, then the scheduler re-plans for New York. On `bcf387d` the UTC sweep ran after New York had been stored.
- `test_failed_sleep_is_retried_not_swept`: a sleep that raises backs off 60s and re-plans; nothing is swept. On `bcf387d` it swept.
- `test_stop_waits_for_the_sleep_to_finish_cancelling`: after `stop()` the sleep task is done and its cancellation cleanup has run. On `bcf387d` it was still pending.
## Deploy note
Both the API and the Home Assistant integration need upgrading. The fix takes effect on the integration's first panel request after the upgrade.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The deployed API host runs UTC and the household `timezone` setting was
never set, so the panel snapshot read UTC: morning quests showed overdue
hours early and `today_iso` rolled over at UTC midnight.
The integration now reports `hass.config.time_zone` on every panel
request (`X-NestQuest-Timezone`). While the admin has not chosen a zone
(`timezone_configured` is false) the API adopts and persists it, so the
panel, the admin plane, and the missed-sweep scheduler all read the
household clock out of the box. An explicit admin choice — including an
explicitly chosen empty zone — still wins; an invalid reported zone is
logged and ignored. No schema change.
Task: 6351b36d-0eef-4e7d-bf5a-96b754cbc883
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The scheduler loaded settings once per cycle and slept on them until the
old rollover, so a zone adopted from Home Assistant after start-up left a
UTC host sweeping at UTC midnight (20:00 in New York), marking quests
missed hours early.
`MissedSweepScheduler.settings_changed` now wakes the sleeping loop when
the stored zone or rollover time differs from the one it planned with;
the panel's household clock calls it after adopting a reported zone.
Before sweeping, the loop re-reads the settings and re-plans instead if
they no longer match its plan.
Tests: the scheduler re-plans for New York midnight when a panel request
adopts the zone mid-sleep, and skips a stale sweep when the zone changed
without a wake-up. The admin-precedence and invalid-header tests now
prove adoption first, and the SSE client test asserts the timezone header
rides beside the Authorization header.
Task: 6351b36d-0eef-4e7d-bf5a-96b754cbc883
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The wake flag compared an adoption only against the plan the loop last
recorded, so New York -> UTC -> New York during the re-plan read left
the scheduler sleeping on UTC midnight; and an adoption committing
between the pre-sweep check and the sweep still let the stale sweep run.
`MissedSweepScheduler.settings_lock` now serialises the panel's adoption
(write plus `settings_changed`) with the loop's plan read and with its
pre-sweep re-read plus sweep. It is never held across the wait, and the
loop holds nothing else while acquiring it, so a panel request is at
most delayed by one settings read or one sweep.
`_sleep_unless_woken` now cancels and awaits both child tasks, and
re-raises a failed sleep inside the loop's retry block, so the backoff
runs instead of a sweep and nothing outlives `stop()`.
Tests (all four fail on bcf387d): the successive-zone race, an adoption
issued during the pre-sweep check, a failing sleep, and stop() waiting
for the sleep's cancellation cleanup.
Task: 6351b36d-0eef-4e7d-bf5a-96b754cbc883
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Maestro task
6351b36d-0eef-4e7d-bf5a-96b754cbc883— "Time zone errors"Done condition: "Tasks will not show overdue and all items will be based on the local time zone. Currently, it seems all times are based on utc."
Root cause
Kids' panel cards → Home Assistant entities → coordinator →
GET /api/v1/panel/snapshot. The API's_household_clock()returnssettings.household_now(_local_now()). When the householdtimezonesetting is empty (as it is in the deployed household),household_nowhands back the host-local instant unchanged, and the deployed host runs UTC.snapshot.pythen evaluatesnow.time() > due_momentagainst UTC, so a 10:00 quest shows overdue from 06:00 Eastern, andtoday_isorolls over at UTC midnight. The only thing that ever filled in the zone automatically was the admin web app's browser auto-set, which runs only when an admin opens it.Fix
hass.config.time_zonein anX-NestQuest-Timezoneheader on every panel request (api_client.py,coordinator.py).settings_store.adopt_reported_timezone(). Whiletimezone_configuredis false, the core saves the reported zone and leavestimezone_configuredfalse, since the value is automatic rather than an admin choice. The check and the write happen under the settings lock.MissedSweepScheduler.settings_changed(settings). If the stored zone or rollover time differs from the scheduler's current plan, this wakes it so it plans again. It is an event, not a poll.cf118cf): a newMissedSweepScheduler.settings_lockis held by the panel adoption across its write and itssettings_changedcall. The loop holds the same lock across (a) its plan read and (b) its pre-sweep re-read plus the sweep. So an adoption commits before a plan read (which sees it), after one (the wake compares against that exact plan), or after the sweep. It never lands between a read and what the loop does with it. The lock is never held across the wait, and the loop holds nothing else while acquiring it, so a panel request cannot deadlock; at most it waits for one settings read or one sweep. The guarantee is documented in the module docstring.cf118cf):_sleep_unless_wokennow cancels and awaits both child tasks, and re-raises a failed sleep inside the loop's retry block. A failing sleep backs off and re-plans instead of sweeping, and nothing outlivesstop().Tests
TZ=UTC uv run pytest -q→ 2168 passed, 1 skipped (headcf118cf)07f2eecand pass here:today_isohass.config.time_zone09d6a55, pass here):07f2eec, pass here):"America/Los_Angeles"and"") first prove automatic adoption, then apply the admin choice and a conflicting reported zone.test_stream_events_reports_household_timezone: the server-sent events request sends both the Authorization and theX-NestQuest-Timezoneheaders.bcf387d, pass here; the new tests were run 15 times with no flakes):test_scheduler_follows_successive_zone_changes: New York → UTC → New York, with a real panel adoption issued while the re-plan read is in flight. The scheduler ends up sleeping on New York midnight (delays 15h, 11h, 15h). Onbcf387dit stays asleep on UTC.test_adoption_after_presweep_check_cannot_precede_the_sweep: a panel adoption is issued right after the pre-sweep read. The sweep only ever runs with the zone it planned for still stored, then the scheduler re-plans for New York. Onbcf387dthe UTC sweep ran after New York had been stored.test_failed_sleep_is_retried_not_swept: a sleep that raises backs off 60s and re-plans; nothing is swept. Onbcf387dit swept.test_stop_waits_for_the_sleep_to_finish_cancelling: afterstop()the sleep task is done and its cancellation cleanup has run. Onbcf387dit was still pending.Deploy note
Both the API and the Home Assistant integration need upgrading. The fix takes effect on the integration's first panel request after the upgrade.
🤖 Generated with Claude Code
Independent Codex gpt-5.6-terra review completed for this exact commit.