Manual timezone is the single source of local-time truth (task 7e173881) #284

Closed
overseer wants to merge 3 commits from task/manual-timezone-authoritative_7e173881-fb99-426f-a2f0-2678439ffc39 into dev
Owner

Maestro task: 7e173881-fb99-426f-a2f0-2678439ffc39 (feature a3cd8e27, Manual application timezone control).

Done-condition: the timezone the admin saves decides local time across the whole application, the kids' panel included. The browser's clock never decides overdue, and an admin save re-plans the missed-sweep scheduler.

A. The panel follows the application timezone

  • frontend/src/cards/quest-log.ts: restores PanelInstance.overdue and renders the API snapshot's overdue flag, which the API computes in the stored zone. This removes the browser-local _isOverdue / wallClockMinutes check added by task fd1093b5.
  • tests/test_panel_cards.py: test_quest_log_overdue_follows_the_api_snapshot_not_the_panel_clock.

B. The scheduler honours the real admin writer

  • api/routes_admin.admin_update_settings holds sweep_scheduler.settings_lock across update_settings, then calls settings_changed(settings).
  • Test: test_admin_settings_patch_replans_a_sleeping_scheduler, with one case for timezone and one for day_rollover_time.

C. Authority across surfaces

  • Every API clock read goes through _household_clock → settings.household_now, and so does the scheduler. An empty timezone means API-host local time, and nothing auto-fills it.
  • Test: test_admin_saved_zone_is_authoritative_across_surfaces (Asia/Kolkata).

D. The visible date and clock follow the application zone (TZ-001, commit bb748c3)

  • The panel snapshot carries timezone. The integration passes it through, and the household due-today sensor publishes it as an attribute. Both cards' _timeZone() format the date and clock in it.
  • The admin app loads the zone once (admin/src/time/AppTimezone.tsx) and uses it for the Today completion clock, History grouping, clocks, lateness and "today", Schedule "today", and new-task default dates. Saving in Settings applies it app-wide straight away.

E. One effective zone for an empty setting (TZ-002, commit e48a058)

  • Problem: with no zone stored, the API used the API host's local time, but the panel fell back to Home Assistant's zone and the admin app to the browser's. Near midnight these could disagree by a day.
  • API: new api/host_zone.py. effective_timezone(stored) returns the stored zone, or else the API host's own IANA zone. The host zone is resolved the way the C library does it: TZ, then the /etc/localtime zoneinfo link, then /etc/timezone, and UTC when none of these is configured. It returns "" only when the host zone has no IANA name.
    • The panel snapshot's timezone is now this effective zone.
    • GET and PATCH /api/v1/admin/settings add a read-only effective_timezone. The stored timezone is unchanged, so the picker still shows "API host local time (no zone set)".
  • Admin app: AppTimezoneProvider and Settings publish effective_timezone. The browser zone applies only if the API publishes none.
  • Cards: already format in the rollup's timezone. Home Assistant's zone now applies only when the rollup has no zone at all (an older API). The comments are updated and the bundle is rebuilt.
  • Tests:
    • test_empty_zone_publishes_the_api_host_zone_to_panel_and_admin: the host is Pacific/Auckland and the clock is 12:30Z. The panel timezone and the admin effective_timezone are both Pacific/Auckland, and today_iso is 2026-09-27, the date the published zone gives. After a zone is saved, both report the saved zone.
    • tests/test_api_host_zone.py: resolver unit tests using temporary /etc trees.
    • The settings route tests pin the host zone and assert effective_timezone.
    • The admin Today, History, Schedule, Definitions and Settings tests now check that an empty stored zone uses the published host zone, and that the browser applies only when no zone is published.

F. An older API payload keeps working (TZ-003, commit e48a058)

  • coordinator.snapshot_from_api_payload now reads payload.get("timezone", "").
  • Test: test_coordinator_reads_a_legacy_payload_without_timezone uses the exact pre-change fixture payload. The refresh succeeds and snapshot.timezone == "".
  • Deployment order: no constraint any more. The integration works against an API older than this change.

Checks (head e48a058)

  • TZ=UTC uv run pytest -q → 2195 passed
  • cd admin && npm test → 339 passed (17 files). npm run build succeeds.
  • cd frontend && npm run build → bundle rebuilt and committed.
  • Base check on bb748c3, in a throwaway worktree (now removed):
    • With only api/host_zone.py added so the tests can import, 16 Python tests fail. They are the settings and snapshot tests expecting effective_timezone, the empty-zone consistency test (KeyError: 'effective_timezone'), and the legacy-payload test (last_update_success is False).
    • 5 admin tests fail: each tab's empty-zone case and the Settings app-wide test.
  • No schema change.

🤖 Generated with Claude Code

Maestro task: `7e173881-fb99-426f-a2f0-2678439ffc39` (feature a3cd8e27, Manual application timezone control). **Done-condition:** the timezone the admin saves decides local time across the whole application, the kids' panel included. The browser's clock never decides overdue, and an admin save re-plans the missed-sweep scheduler. ## A. The panel follows the application timezone - `frontend/src/cards/quest-log.ts`: restores `PanelInstance.overdue` and renders the API snapshot's `overdue` flag, which the API computes in the stored zone. This removes the browser-local `_isOverdue` / `wallClockMinutes` check added by task fd1093b5. - `tests/test_panel_cards.py`: `test_quest_log_overdue_follows_the_api_snapshot_not_the_panel_clock`. ## B. The scheduler honours the real admin writer - `api/routes_admin.admin_update_settings` holds `sweep_scheduler.settings_lock` across `update_settings`, then calls `settings_changed(settings)`. - Test: `test_admin_settings_patch_replans_a_sleeping_scheduler`, with one case for `timezone` and one for `day_rollover_time`. ## C. Authority across surfaces - Every API clock read goes through `_household_clock` → `settings.household_now`, and so does the scheduler. An empty `timezone` means API-host local time, and nothing auto-fills it. - Test: `test_admin_saved_zone_is_authoritative_across_surfaces` (Asia/Kolkata). ## D. The visible date and clock follow the application zone (TZ-001, commit bb748c3) - The panel snapshot carries `timezone`. The integration passes it through, and the household due-today sensor publishes it as an attribute. Both cards' `_timeZone()` format the date and clock in it. - The admin app loads the zone once (`admin/src/time/AppTimezone.tsx`) and uses it for the Today completion clock, History grouping, clocks, lateness and "today", Schedule "today", and new-task default dates. Saving in Settings applies it app-wide straight away. ## E. One effective zone for an empty setting (TZ-002, commit e48a058) - **Problem:** with no zone stored, the API used the API host's local time, but the panel fell back to Home Assistant's zone and the admin app to the browser's. Near midnight these could disagree by a day. - **API:** new `api/host_zone.py`. `effective_timezone(stored)` returns the stored zone, or else the API host's own IANA zone. The host zone is resolved the way the C library does it: `TZ`, then the `/etc/localtime` zoneinfo link, then `/etc/timezone`, and UTC when none of these is configured. It returns `""` only when the host zone has no IANA name. - The panel snapshot's `timezone` is now this effective zone. - `GET` and `PATCH /api/v1/admin/settings` add a read-only `effective_timezone`. The stored `timezone` is unchanged, so the picker still shows "API host local time (no zone set)". - **Admin app:** `AppTimezoneProvider` and Settings publish `effective_timezone`. The browser zone applies only if the API publishes none. - **Cards:** already format in the rollup's `timezone`. Home Assistant's zone now applies only when the rollup has no zone at all (an older API). The comments are updated and the bundle is rebuilt. - **Tests:** - `test_empty_zone_publishes_the_api_host_zone_to_panel_and_admin`: the host is Pacific/Auckland and the clock is 12:30Z. The panel `timezone` and the admin `effective_timezone` are both Pacific/Auckland, and `today_iso` is 2026-09-27, the date the published zone gives. After a zone is saved, both report the saved zone. - `tests/test_api_host_zone.py`: resolver unit tests using temporary `/etc` trees. - The settings route tests pin the host zone and assert `effective_timezone`. - The admin Today, History, Schedule, Definitions and Settings tests now check that an empty stored zone uses the published host zone, and that the browser applies only when no zone is published. ## F. An older API payload keeps working (TZ-003, commit e48a058) - `coordinator.snapshot_from_api_payload` now reads `payload.get("timezone", "")`. - Test: `test_coordinator_reads_a_legacy_payload_without_timezone` uses the exact pre-change fixture payload. The refresh succeeds and `snapshot.timezone == ""`. - **Deployment order:** no constraint any more. The integration works against an API older than this change. ## Checks (head e48a058) - `TZ=UTC uv run pytest -q` → 2195 passed - `cd admin && npm test` → 339 passed (17 files). `npm run build` succeeds. - `cd frontend && npm run build` → bundle rebuilt and committed. - **Base check on `bb748c3`, in a throwaway worktree (now removed):** - With only `api/host_zone.py` added so the tests can import, 16 Python tests fail. They are the settings and snapshot tests expecting `effective_timezone`, the empty-zone consistency test (`KeyError: 'effective_timezone'`), and the legacy-payload test (`last_update_success` is False). - 5 admin tests fail: each tab's empty-zone case and the Settings app-wide test. - No schema change. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
overseer added 1 commit 2026-09-30 07:51:55 -04:00
The kids' Quest Log again renders each instance's overdue state from
the API snapshot's `overdue` flag, which the API computes in the stored
application timezone. The browser-local overdue check from task
fd1093b5 (`_isOverdue` against `_now`) is removed; `_now` only drives
the header clock and date.

The admin settings route (PATCH /api/v1/admin/settings), the only
settings writer, now holds the missed-sweep scheduler's settings_lock
across the write and calls settings_changed, so saving `timezone` or
`day_rollover_time` re-plans a sleeping scheduler (review finding
TZ-001). An app without a scheduler just writes.

Tests: the card shows the snapshot's overdue state whatever the
browser's zone or clock says; an admin PATCH of the zone or the
rollover re-plans a sleeping scheduler; one saved zone
(Asia/Kolkata) is read identically by the panel snapshot, the admin
snapshot and the scheduler delay.

Task: 7e173881-fb99-426f-a2f0-2678439ffc39

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
overseer added 1 commit 2026-09-30 08:17:29 -04:00
The panel snapshot now carries the admin-saved `timezone` ("" when unset);
the coordinator threads it onto the household due-today sensor, and the
Party Board and Quest Log cards format their kicker, header and dock
date and clock in it, falling back to Home Assistant's zone only when
none is stored. The admin app loads the stored zone once
(AppTimezoneProvider) and uses it for completion clocks, History day
grouping, clocks and lateness, and "today" in Schedule, History and new
task defaults; Settings publishes a saved zone app-wide at once.

Resolves review finding TZ-001 (task 7e173881).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
overseer added 1 commit 2026-09-30 08:44:27 -04:00
TZ-002: with no zone stored the API reads its clock in the host's local
time, but the panel fell back to Home Assistant's zone and the admin app
to the browser's, so dates could disagree near midnight. The API now
names the zone it actually uses (api/host_zone.py: the stored zone, else
the host's own IANA zone from TZ, /etc/localtime or /etc/timezone) and
publishes it as the panel snapshot's `timezone` and the admin settings'
read-only `effective_timezone`. Both cards and the admin app format in
that one zone; Home Assistant's or the browser's zone applies only when
none is published.

TZ-003: snapshot_from_api_payload reads `timezone` with a "" default, so
an integration upgraded ahead of its API keeps refreshing against the
pre-change payload.

Task 7e173881-fb99-426f-a2f0-2678439ffc39

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Pull request closed

Please reopen this pull request to perform a merge.
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Maniacs_Mansion/NestQuest#284