---
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 <input type="text" list="..."> — 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; `<input type="text" list="iana-zones">` + `<datalist>` 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, `<section aria-label="Timezone">` 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.
- `<input type="text" list="iana-zones">` 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*
