--- phase: 18-auto-timezone-detection-and-ability-to-change-timezone plan: "04" subsystem: pwa tags: [timezone, iana, admin, react, tanstack-query, playwright, ui] # Dependency graph requires: - phase: 18-02 provides: GET/PUT /api/admin/config/timezone (consumed by fetchAdminTimezone/setAdminTimezone) provides: - fetchAdminTimezone() — GET /api/admin/config/timezone in PWA API client - setAdminTimezone(tz) — PUT /api/admin/config/timezone in PWA API client - AdminTimezoneResponse interface (timezone: string, isExplicitlySet: boolean) - Timezone section in AdminPage (/admin) — searchable IANA picker, save, detected-zone affordance affects: - apps/pwa/src/api/client.ts - apps/pwa/src/routes/AdminPage.tsx - apps/pwa/e2e/timezone-verify.spec.ts (new verification spec) # Tech tracking tech-stack: added: [] patterns: - Intl.supportedValuesOf('timeZone') guarded for runtime availability (datalist population) - ARIA combobox role for — Playwright locator uses getByRole('combobox') not getByRole('textbox') - useQuery + useMutation with invalidateQueries on success (same pattern as calendars section) key-files: created: - apps/pwa/e2e/timezone-verify.spec.ts modified: - apps/pwa/src/api/client.ts - apps/pwa/src/routes/AdminPage.tsx key-decisions: - "Removed AdminTimezoneResponse from AdminPage.tsx import list — ESLint no-unused-vars; type is inferred from useQuery return" - "Input with list= attribute has ARIA combobox role in Chromium, not textbox — Playwright e2e uses getByRole('combobox')" - "timezoneInput state is null when the user has not interacted — effectiveTimezoneInput = timezoneInput ?? storedTimezone preserves stored value as pre-fill" - "setTimezoneInput(null) on mutation success resets local override so save button re-disables to match new stored value" requirements-completed: [D-02, D-04] # Metrics duration: 15min completed: 2026-06-15 --- # Phase 18 Plan 04: Admin Timezone UI Summary **Searchable IANA timezone picker in /admin Settings — fetchAdminTimezone/setAdminTimezone in client.ts + Timezone section in AdminPage.tsx — verified end-to-end via Playwright with the real 18-02 endpoints** ## Performance - **Duration:** ~15 min - **Started:** 2026-06-15T02:30:00Z - **Completed:** 2026-06-15T02:45:03Z - **Tasks:** 3 (2 auto + 1 playwright-verified checkpoint) - **Files modified/created:** 3 ## Accomplishments - Added `AdminTimezoneResponse` interface + `fetchAdminTimezone()` + `setAdminTimezone()` to `apps/pwa/src/api/client.ts`, following the exact pattern of `fetchAdminCalendars` / `setSharedCalendar` (credentials: include, redirect: manual, handleAuthResponse) - Added Timezone section to `AdminPage.tsx` after the Shared Calendar section (with `marginBottom` on the preceding section for spacing) - Timezone section features: 4-state loading/error/data pattern; `` + `` from `Intl.supportedValuesOf('timeZone')` (guarded); "Using system default" notice when `isExplicitlySet=false`; "Use detected: {zone}" affordance (D-02); disabled Save when pending or unchanged; `timezoneMutation.mutate(tz)` on save; `invalidateQueries(['admin','timezone'])` on success - Created `apps/pwa/e2e/timezone-verify.spec.ts` with 6 desktop Playwright tests — all 6 pass against the live 18-02 API endpoints, including the save + persist across reload flow ## Task Commits 1. **Task 1: Add fetchAdminTimezone + setAdminTimezone to PWA API client** - `57424e6` (feat) 2. **Task 2: Add Timezone section to AdminPage** - `43d6689` (feat) 3. **Task 3: Playwright timezone round-trip e2e spec** - `3013b53` (test) ## Files Created/Modified - `apps/pwa/src/api/client.ts` — added `AdminTimezoneResponse` interface + `fetchAdminTimezone()` + `setAdminTimezone()` in the `/api/admin/*` section - `apps/pwa/src/routes/AdminPage.tsx` — added `timezoneQuery`, `timezoneMutation`, `timezoneInput` state, `detectedTz`, computed values, `
` with IANA picker + affordances + save button - `apps/pwa/e2e/timezone-verify.spec.ts` — new 6-test Playwright spec verifying the full round-trip ## Decisions Made - `AdminTimezoneResponse` type import removed from `AdminPage.tsx` — `@typescript-eslint/no-unused-vars` flagged it (type is inferred from `useQuery` return value, not used as an explicit annotation). ESLint clean. - `` has ARIA `combobox` role (not `textbox`) in Chromium — discovered via Playwright page snapshot. Updated e2e locators to `getByRole('combobox')`. - The `timezoneInput` state variable is `null` when the user hasn't typed anything; `effectiveTimezoneInput = timezoneInput ?? storedTimezone` ensures the input shows the stored value on load without the save button enabling prematurely. - After mutation success, `setTimezoneInput(null)` resets the local override so the save button re-disables (effectiveTimezoneInput collapses back to the now-updated storedTimezone). ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 1 - Bug] Removed unused `AdminTimezoneResponse` import** - **Found during:** Task 2 (ESLint check) - **Issue:** `type AdminTimezoneResponse` was imported but not used as an explicit type annotation; ESLint `no-unused-vars` reported it as an error - **Fix:** Removed from the import line; the type is inferred via `useQuery` - **Files modified:** `apps/pwa/src/routes/AdminPage.tsx` - **Commit:** `43d6689` **2. [Rule 3 - Blocking] API dev container serving stale compiled code** - **Found during:** Task 3 (playwright verification) - **Issue:** The Docker container running the API had the pre-Plan-18-02 `dist/routes/admin.js` baked in — the timezone endpoints returned 404. The `--watch dist/index.js` watcher only restarts on `index.js` change, not sub-module changes. - **Fix:** Rebuilt the API (`pnpm --filter @familysync/api build`), then `docker cp` the updated `dist/routes/admin.js`, `dist/lib/householdTimezone.js`, and `dist/index.js` into the container to trigger a restart. - **Files modified:** container-only (not tracked in git) - **Commit:** N/A (operational, not code) ## Known Stubs None — the Timezone section is fully wired to the real `GET /api/admin/config/timezone` and `PUT /api/admin/config/timezone` endpoints (Plan 18-02). No placeholder data. ## Threat Flags No new threat surface beyond the plan's threat model. T-18-11 (client isAdmin gate is UX only), T-18-12 (server validates IANA via Plan 02 Zod refine), T-18-13 (plain-text JSX children, no dangerouslySetInnerHTML). ## Self-Check: PASSED - `apps/pwa/src/api/client.ts` — FOUND (modified, exports fetchAdminTimezone + setAdminTimezone + AdminTimezoneResponse) - `apps/pwa/src/routes/AdminPage.tsx` — FOUND (modified, contains `aria-label="Timezone"`) - `apps/pwa/e2e/timezone-verify.spec.ts` — FOUND (created, 6 desktop tests all pass) - Commit `57424e6` — FOUND - Commit `43d6689` — FOUND - Commit `3013b53` — FOUND --- *Phase: 18-auto-timezone-detection-and-ability-to-change-timezone* *Completed: 2026-06-15*