From cf170214a6afd54a2604d5356fd305a539991331 Mon Sep 17 00:00:00 2001 From: dev_agent Date: Wed, 30 Sep 2026 09:50:25 -0400 Subject: [PATCH] Format the panel and admin date and clock in the stored application timezone 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 --- admin/src/App.test.tsx | 53 +++++++-- admin/src/App.tsx | 22 ++-- admin/src/definitions/DefinitionsTab.test.tsx | 54 ++++++++- admin/src/definitions/DefinitionsTab.tsx | 42 ++++--- admin/src/history/HistoryTab.test.tsx | 112 ++++++++++++++++++ admin/src/history/HistoryTab.tsx | 18 +-- admin/src/history/format.ts | 45 ++++--- admin/src/schedule/OverrideEditor.test.tsx | 2 + admin/src/schedule/ScheduleTab.test.tsx | 55 ++++++++- admin/src/schedule/ScheduleTab.tsx | 9 +- admin/src/schedule/cycle.ts | 7 -- admin/src/settings/SettingsScreen.test.tsx | 40 +++++++ admin/src/settings/SettingsScreen.tsx | 10 +- admin/src/time/AppTimezone.tsx | 91 ++++++++++++++ admin/src/time/zone.test.ts | 60 ++++++++++ admin/src/time/zone.ts | 79 ++++++++++++ admin/src/today/TodayTab.test.tsx | 54 +++++++++ admin/src/today/TodayTab.tsx | 4 +- admin/src/today/format.ts | 14 ++- api/routes_panel.py | 12 +- custom_components/nestquest/coordinator.py | 3 + custom_components/nestquest/core/snapshot.py | 5 + custom_components/nestquest/sensor.py | 3 + .../nestquest/www/nestquest-cards.js | 60 ++++++---- frontend/src/cards/party-board.ts | 10 +- frontend/src/cards/quest-log.ts | 10 +- frontend/tests/render-party-board.mjs | 25 +++- tests/conftest.py | 1 + tests/test_api_admin_snapshot.py | 6 +- tests/test_api_household_clock.py | 18 +++ tests/test_binary_sensors.py | 1 + tests/test_complete_quest_proxy.py | 1 + tests/test_coordinator.py | 8 +- tests/test_core_snapshot.py | 1 + tests/test_entity_lifecycle.py | 1 + tests/test_entity_matrix.py | 1 + tests/test_events.py | 1 + tests/test_panel_cards.py | 74 +++++++++++- tests/test_sensors.py | 17 +++ tests/test_sse_subscription.py | 7 +- 40 files changed, 910 insertions(+), 126 deletions(-) create mode 100644 admin/src/time/AppTimezone.tsx create mode 100644 admin/src/time/zone.test.ts create mode 100644 admin/src/time/zone.ts diff --git a/admin/src/App.test.tsx b/admin/src/App.test.tsx index 7a38e6a..442f2ee 100644 --- a/admin/src/App.test.tsx +++ b/admin/src/App.test.tsx @@ -2,10 +2,28 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { cleanup, fireEvent, render, screen } from "@testing-library/react"; import App from "./App"; +function jsonResponse(body: unknown, status = 200): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "Content-Type": "application/json" }, + }); +} + +let settingsResponse: () => Promise; + describe("NestQuest Admin shell", () => { beforeEach(() => { - // The Today tab fetches the admin snapshot; keep the shell tests offline. - vi.stubGlobal("fetch", vi.fn(() => new Promise(() => {}))); + settingsResponse = () => Promise.resolve(jsonResponse({ timezone: "" })); + // The shell loads the stored application timezone; every tab's own + // requests stay pending so the shell tests remain offline. + vi.stubGlobal( + "fetch", + vi.fn((url: string) => + String(url).endsWith("/api/v1/admin/settings") + ? settingsResponse() + : new Promise(() => {}), + ), + ); }); afterEach(() => { @@ -22,10 +40,10 @@ describe("NestQuest Admin shell", () => { } }); - it("starts on the Today screen and switches on tab click", () => { + it("starts on the Today screen and switches on tab click", async () => { render(); - expect(screen.getByRole("heading", { name: "Today" })).toBeTruthy(); + expect(await screen.findByRole("heading", { name: "Today" })).toBeTruthy(); fireEvent.click(screen.getByRole("tab", { name: "History" })); @@ -36,12 +54,33 @@ describe("NestQuest Admin shell", () => { ); }); - it("the Tasks tab renders the definitions screen", () => { + it("the Tasks tab renders the definitions screen", async () => { render(); fireEvent.click(screen.getByRole("tab", { name: "Tasks" })); - expect(screen.getByRole("heading", { name: "Tasks" })).toBeTruthy(); + expect(await screen.findByRole("heading", { name: "Tasks" })).toBeTruthy(); expect(screen.getByRole("tabpanel").textContent).not.toContain("placeholder"); }); -}); \ No newline at end of file + + it("renders no screen until the stored application timezone is known", async () => { + settingsResponse = () => new Promise(() => {}); + render(); + + expect(screen.getByTestId("timezone-loading")).toBeTruthy(); + expect(screen.queryByRole("heading", { name: "Today" })).toBeNull(); + }); + + it("a failed timezone load says so and retries", async () => { + settingsResponse = () => Promise.resolve(jsonResponse({ detail: "boom" }, 500)); + render(); + + await screen.findByTestId("timezone-error"); + expect(screen.queryByRole("heading", { name: "Today" })).toBeNull(); + + settingsResponse = () => Promise.resolve(jsonResponse({ timezone: "Pacific/Auckland" })); + fireEvent.click(screen.getByRole("button", { name: "Try again" })); + + expect(await screen.findByRole("heading", { name: "Today" })).toBeTruthy(); + }); +}); diff --git a/admin/src/App.tsx b/admin/src/App.tsx index 5f44fad..31ab78a 100644 --- a/admin/src/App.tsx +++ b/admin/src/App.tsx @@ -4,6 +4,7 @@ import { TransitionsProvider } from "./api/useTransitions"; import DefinitionsTab from "./definitions/DefinitionsTab"; import HistoryTab from "./history/HistoryTab"; import ScheduleTab from "./schedule/ScheduleTab"; +import { AppTimezoneProvider } from "./time/AppTimezone"; import TodayTab from "./today/TodayTab"; const TABS = [ @@ -32,15 +33,18 @@ export default function App() { role="tabpanel" aria-labelledby={`tab-${active}`} > - {active === "today" ? ( - - ) : active === "tasks" ? ( - - ) : active === "schedule" ? ( - - ) : ( - - )} + {/* Every screen's local time is the stored application timezone. */} + + {active === "today" ? ( + + ) : active === "tasks" ? ( + + ) : active === "schedule" ? ( + + ) : ( + + )} +