Files
2026-06-18 22:21:38 -04:00

12 KiB
Raw Permalink Blame History

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
18-auto-timezone-detection-and-ability-to-change-timezone 04 execute 3
18-02
apps/pwa/src/api/client.ts
apps/pwa/src/routes/AdminPage.tsx
false
D-02
D-04
truths artifacts key_links
An admin sees a Timezone section in /admin Settings showing the current household timezone (D-04)
The admin can search/pick an IANA zone and save it; the change persists across reload (D-04)
The picker pre-offers the browser-detected zone (Intl.DateTimeFormat().resolvedOptions().timeZone) so first-run seeding is one tap (D-02)
The section indicates when the timezone is using the system default (isExplicitlySet=false)
path provides exports
apps/pwa/src/api/client.ts fetchAdminTimezone() + setAdminTimezone() + AdminTimezoneResponse
fetchAdminTimezone
setAdminTimezone
AdminTimezoneResponse
path provides
apps/pwa/src/routes/AdminPage.tsx Timezone section (searchable IANA picker + save) after Shared Calendar section
from to via pattern
apps/pwa/src/routes/AdminPage.tsx /api/admin/config/timezone useQuery(fetchAdminTimezone) + useMutation(setAdminTimezone) fetchAdminTimezone|setAdminTimezone
from to via pattern
apps/pwa/src/api/client.ts PUT /api/admin/config/timezone fetch with JSON body PUT.*config/timezone
Add the admin-facing Timezone UI: a searchable IANA picker in the existing `/admin` Settings page, backed by two new client functions that call the Plan 02 endpoints. The picker pre-offers the browser-detected zone so an admin (or the Phase 12 wizard, later) can seed the household timezone in one tap.

Purpose: D-04 surfaces the change-timezone control in the role-gated admin Settings; D-02 wires the browser detection (Intl.DateTimeFormat().resolvedOptions().timeZone) as the suggested value. This is UI + glue against the contract Plan 02 already established and tested. Output: fetchAdminTimezone/setAdminTimezone in client.ts + a Timezone section in AdminPage.tsx, verified end-to-end with playwright-cli.

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/18-auto-timezone-detection-and-ability-to-change-timezone/18-CONTEXT.md @.planning/phases/18-auto-timezone-detection-and-ability-to-change-timezone/18-PATTERNS.md @apps/pwa/src/api/client.ts @apps/pwa/src/routes/AdminPage.tsx Task 1: Add fetchAdminTimezone + setAdminTimezone to the PWA API client apps/pwa/src/api/client.ts - apps/pwa/src/api/client.ts (lines ~413469: fetchAdminMembers / saveCredential / fetchAdminCalendars / setSharedCalendar — exact GET + PUT-with-body patterns; handleAuthResponse at lines ~5158; the `// ── /api/admin/* ──` section header at line 355) - .planning/phases/18-auto-timezone-detection-and-ability-to-change-timezone/18-PATTERNS.md (client.ts section: AdminTimezoneResponse interface + both function bodies) In the `// ── /api/admin/* ──` section of client.ts, export `interface AdminTimezoneResponse { timezone: string; isExplicitlySet: boolean }`. Add `fetchAdminTimezone()` — GET `/api/admin/config/timezone` with `credentials: 'include', redirect: 'manual'`, call `handleAuthResponse(res, 'GET /api/admin/config/timezone')`, return `res.json()` as `AdminTimezoneResponse`. Add `setAdminTimezone(timezone: string)` — PUT `/api/admin/config/timezone` with `Content-Type: application/json`, `credentials: 'include', redirect: 'manual'`, body `JSON.stringify({ timezone })`, then `handleAuthResponse(res, 'PUT /api/admin/config/timezone')`. Match the existing wrappers' style exactly. pnpm --filter @familysync/pwa exec tsc --noEmit && grep -q "fetchAdminTimezone" apps/pwa/src/api/client.ts && grep -q "PUT" apps/pwa/src/api/client.ts - client.ts exports `fetchAdminTimezone`, `setAdminTimezone`, and `AdminTimezoneResponse`. - Both wrappers call `handleAuthResponse` and use `credentials: 'include', redirect: 'manual'` (same auth handling as sibling admin calls). - `pnpm --filter @familysync/pwa exec tsc --noEmit` passes. Client functions compile and mirror the established admin fetch wrappers. Task 2: Add the Timezone section (searchable IANA picker + save) to AdminPage apps/pwa/src/routes/AdminPage.tsx - apps/pwa/src/routes/AdminPage.tsx (Shared Calendar section lines ~183288: section structure, sectionLabelStyle at ~40, calendarsQuery at ~72, sharedCalMutation at ~86, save-button style ~244285) - apps/pwa/src/api/client.ts (the fetchAdminTimezone / setAdminTimezone / AdminTimezoneResponse added in Task 1) - .planning/phases/18-auto-timezone-detection-and-ability-to-change-timezone/18-PATTERNS.md (AdminPage.tsx section: query/mutation/section template, datalist picker UX, import additions) - CLAUDE.md (Browser-based verification convention — use playwright-cli, NOT @playwright/test, for the manual check) Import `fetchAdminTimezone, setAdminTimezone, type AdminTimezoneResponse` from `../api/client.js`. Add `timezoneQuery = useQuery({ queryKey: ['admin','timezone'], queryFn: fetchAdminTimezone, retry: false, staleTime: 60*1000 })` and `timezoneMutation = useMutation({ mutationFn: (tz: string) => setAdminTimezone(tz), onSuccess: () => void queryClient.invalidateQueries({ queryKey: ['admin','timezone'] }) })`. Add a `
` after the Shared Calendar section (give the preceding section `marginBottom: 'var(--space-8, 32px)'`), with `
Timezone
` and the four states (loading / error / data). Picker: a controlled `` + `` populated from `Intl.supportedValuesOf('timeZone')` (guard for absence → empty list). Initialize the input from `timezoneQuery.data?.timezone`. Compute `const detectedTz = Intl.DateTimeFormat().resolvedOptions().timeZone` and render a small "Use detected: {detectedTz}" affordance that sets the input to detectedTz (D-02 one-tap seed). When `timezoneQuery.data?.isExplicitlySet === false`, show a subtle "using system default" note. Save button (reuse the existing save-button style; disabled while pending or when the input equals the stored value) calls `timezoneMutation.mutate(inputValue)`; label `timezoneMutation.isPending ? 'Saving…' : 'Save'`. Do NOT touch any other section or the display/timed-event path (D-07). pnpm --filter @familysync/pwa exec tsc --noEmit && pnpm --filter @familysync/pwa test -- --run && grep -q 'aria-label="Timezone"' apps/pwa/src/routes/AdminPage.tsx - AdminPage renders a `
` with the stored value pre-filled and a datalist-backed searchable input. - A "Use detected: " affordance sets the input to `Intl.DateTimeFormat().resolvedOptions().timeZone` (D-02). - Save calls setAdminTimezone and invalidates the ['admin','timezone'] query on success; the section shows the "system default" note when isExplicitlySet is false. - `pnpm --filter @familysync/pwa exec tsc --noEmit` and `pnpm --filter @familysync/pwa test -- --run` pass. - No change to eventDateTime.ts / hydrateEvents.ts / any other AdminPage section (D-07). Timezone section renders, saves, persists, and offers the detected zone. Task 3: playwright-cli round-trip of the admin timezone picker apps/pwa/src/routes/AdminPage.tsx A Timezone section in /admin Settings: searchable IANA picker, "use detected zone" affordance, save + persist via the Plan 02 endpoints. Drive the verification with the global `playwright-cli` binary (NOT @playwright/test) against the host-side dev stack with DEV_AUTH_BYPASS (the dev-bypass user must be admin — see the MEMORY dev-bypass note; unlock admin if the section is not visible). This is an automation-first check the executor runs, then presents the result to the operator for sign-off. 1. Open the PWA, navigate to /admin Settings. 2. Confirm a "Timezone" section shows the current household timezone (or "system default"). 3. Type a city (e.g. "Chicago"), pick "America/Chicago" from the datalist, click Save. 4. Reload /admin and confirm the section now shows "America/Chicago" with no "system default" note. 5. Confirm the "Use detected: " affordance fills the input with the browser zone. Expected: the value persists across reload; no console errors; the save button disables while pending. playwright-cli round-trip confirms the timezone saves and persists across reload, and the detected-zone affordance works. Type "approved" or describe what rendered/persisted incorrectly. Operator confirms the admin timezone picker saves, persists across reload, and offers the detected zone.

<threat_model>

Trust Boundaries

Boundary Description
browser PWA → /api/admin/config/timezone the UI is a convenience; the server requireAdmin + IANA validation (Plan 02) is the real boundary

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-18-11 Elevation of Privilege client renders admin UI based on isAdmin flag accept Client isAdmin is UX-only (Phase 10 D-03); a non-admin who forges the request still hits server-side requireAdmin → 403 (Plan 02 T-18-03). The UI gate is not a security control and is not relied upon as one.
T-18-12 Tampering user types a non-IANA string into the free-text input mitigate The datalist offers only valid zones; a manually-typed invalid string is rejected by the server's Zod isValidIanaTimezone refine (Plan 02) → 400 surfaced to the user. Client may additionally pre-check before enabling Save, but the server is authoritative.
T-18-13 Information Disclosure XSS via rendered timezone value mitigate The value is rendered as plain-text JSX children (no dangerouslySetInnerHTML), consistent with the existing AdminPage convention. IANA strings are constrained anyway.
T-18-SC Tampering npm/pip/cargo installs mitigate No new packages this plan (RESEARCH Package Legitimacy Audit: zero installs; picker uses built-in Intl).
</threat_model>
- `pnpm --filter @familysync/pwa exec tsc --noEmit` green. - `pnpm --filter @familysync/pwa test -- --run` green. - playwright-cli round-trip (Task 3) confirms save + persist + detected-zone affordance.

<success_criteria>

  • Admin Timezone section reads/writes the household timezone and persists across reload (D-04).
  • Browser-detected zone offered for one-tap seeding (D-02).
  • No regression to other sections or the display/timed-write path (D-07). </success_criteria>
Create `.planning/phases/18-auto-timezone-detection-and-ability-to-change-timezone/18-04-SUMMARY.md` when done.