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

15 KiB

phase, verified, status, score, overrides_applied, browser_verification
phase verified status score overrides_applied browser_verification
18-auto-timezone-detection-and-ability-to-change-timezone 2026-06-14T23:05:00Z passed 7/7 0
test result note
Admin timezone picker round-trip in browser PASSED — timezone-verify.spec.ts (6 tests, desktop/Chromium) re-run against the live dev stack (DEV_AUTH_BYPASS=true) after rebuilding the API container. Confirmed: Timezone section renders, input pre-filled, first-run Save enabled (WR-01 fix), changing input enables Save, save persists across reload with the 'Using system default' note disappearing, and 'Use detected' pre-fills the browser zone. Surfaced and fixed a test-determinism gap: e2e global-setup did not clear app_config.household_timezone, leaking a prior run's value. Fixed in commit ddeb87c (clear the key in global-setup + align the stale Save-disabled assertion with the WR-01 first-run behaviour).

Phase 18: Auto Timezone Detection and Ability to Change Timezone — Verification Report

Phase Goal: Make the household timezone an explicit, stored, user-changeable setting — auto-detected from the browser at first run, changeable from the role-gated /admin Settings — and route the server-side all-day "9 AM local" reminder computation through it (replacing the implicit process.env.TZ fallback), without touching the already-correct browser-local display/timed-write path.

Decision contract: D-01 stored in app_config; D-02 auto-detect/seed from browser at first run; D-03 seed must NOT overwrite an explicit value; D-04 changeable from role-gated /admin; D-05 single shared accessor is the source of truth; D-06 fallback chain (stored ?? process.env.TZ ?? Intl resolved zone) when unset; D-07 do NOT touch the browser-local display/timed-write path.

Verified: 2026-06-14T23:05:00Z Status: passed (all automated checks pass; browser round-trip re-confirmed via Playwright e2e against the live stack) Re-verification: No — initial verification


Goal Achievement

Observable Truths (Decision Contract)

# Decision Truth Status Evidence
1 D-01 Household timezone stored as household_timezone key in app_config VERIFIED admin.ts:229db.insert(appConfig).values({ key: 'household_timezone', value: timezone }).onDuplicateKeyUpdate(...)
2 D-02 Browser-detected zone seeded at first run via POST /api/admin/config/timezone/seed VERIFIED admin.ts:248-273 — seed endpoint; AdminPage.tsx:108detectedTz = Intl.DateTimeFormat().resolvedOptions().timeZone; seed endpoint called from PWA
3 D-03 Seed does NOT overwrite an explicit value VERIFIED admin.ts:257-271 — SELECT-before-INSERT with onDuplicateKeyUpdate({ set: { value: sql\value` } })(WR-02 fix). Tests atadmin.test.ts:753-806` confirm no-overwrite and idempotency under race.
4 D-04 Timezone changeable from role-gated /admin only VERIFIED admin.ts:42adminRouter.use('*', requireAdmin) is first statement; all three timezone routes registered after line 42 inherit the guard. Tests at admin.test.ts:603-625 assert 403 for non-admin on GET and PUT. POST seed 403 test at line 711.
5 D-05 Single shared accessor getHouseholdTimezone(db) is the only TZ read site for brokers VERIFIED reminderScheduler.ts:51,250 — import + await getHouseholdTimezone(db). outboxWorker.ts:46,504,612 — import + two call sites. No bare process.env.TZ ?? Intl expression remains at any call site (only in comments). No stragglers confirmed by grep returning zero non-comment hits.
6 D-06 Fallback chain: stored ?? process.env.TZ ?? Intl resolved zone VERIFIED householdTimezone.ts:34-38row?.value ?? process.env.TZ ?? Intl.DateTimeFormat().resolvedOptions().timeZone. 11 unit tests in householdTimezone.test.ts cover all four branches (stored / TZ env / Intl / null-value fall-through).
7 D-07 eventDateTime.ts and hydrateEvents.ts NOT modified VERIFIED git diff --name-only origin/main..HEAD does not list either file. Output confirmed: "CONFIRMED: Neither file appears in branch diff".

Score: 7/7 truths verified (automated)


Required Artifacts

Artifact Expected Status Details
apps/api/src/lib/householdTimezone.ts Shared TZ accessor + IANA validator (D-05, D-06) VERIFIED Exists, 55 lines, exports getHouseholdTimezone and isValidIanaTimezone. Verbatim D-06 fallback chain. No Intl.supportedValuesOf (avoids UTC-omission pitfall).
apps/api/tests/lib/householdTimezone.test.ts Unit tests for accessor fallback chain + validator VERIFIED Exists. 11 tests. Covers stored row, no-row + TZ env, no-row + no-TZ, null-value fall-through, valid zones (incl. 'UTC'), and invalid zones.
apps/api/src/routes/admin.ts GET + PUT + seed timezone endpoints under requireAdmin VERIFIED Exists. timezoneSchema with isValidIanaTimezone refine. GET at line 200, PUT at 224, seed at 248. All after line-42 requireAdmin.use('*', ...). WR-02 fix applied (onDuplicateKeyUpdate in seed).
apps/api/tests/routes/admin.test.ts Integration tests: 403 non-admin, IANA 400/200, round-trip, seed no-overwrite VERIFIED Exists. 8 original timezone cases + 3 WR-02 cases added post-review (403 on seed, seeded:true on first, idempotent second).
apps/api/src/broker/reminderScheduler.ts All-day TZ rewired to getHouseholdTimezone (D-05) VERIFIED Line 51: import. Line 250: const serverTz = await getHouseholdTimezone(db). Line 249 comment references D-06.
apps/api/src/broker/outboxWorker.ts Both all-day TZ sites rewired to getHouseholdTimezone (D-05) VERIFIED Line 46: import. Line 504: CREATE branch const tz = await getHouseholdTimezone(db). Line 612: UPDATE branch same. Both in comment references D-06.
apps/pwa/src/api/client.ts fetchAdminTimezone() + setAdminTimezone() + AdminTimezoneResponse VERIFIED Lines 476-511. Interface defined at 476. GET wrapper at 485 with credentials: 'include', redirect: 'manual', handleAuthResponse. PUT wrapper at 501 with Content-Type header and body.
apps/pwa/src/routes/AdminPage.tsx Timezone section with searchable IANA picker, save, detected-zone affordance VERIFIED <section aria-label="Timezone"> at line 342. timezoneQuery at line 93 with 60s staleTime. timezoneMutation at line 100. detectedTz at line 108. WR-01 fix at lines 114-128 (isExplicit && ... guard). "Use detected" affordance at line 415. "Using system default" notice at line 372.
apps/pwa/src/routes/AdminPage.timezone.test.ts Unit tests for WR-01 save-enabled logic VERIFIED Exists. Tests at line 44 and 57 cover first-run (isExplicitlySet: false) save-enabled case.
apps/pwa/e2e/timezone-verify.spec.ts Playwright e2e for browser round-trip VERIFIED (exists) 6 test cases: section visible, combobox pre-filled, save disabled when unchanged, changing enables save, persist across reload, "Use detected" affordance. Ran green at execution time against live stack. Cannot re-run without live Docker stack.

From To Via Status Details
householdTimezone.ts app_config row household_timezone Drizzle select with eq(appConfig.key, 'household_timezone') VERIFIED Line 29-32: .select({ value: appConfig.value }).from(appConfig).where(eq(appConfig.key, 'household_timezone')).limit(1)
admin.ts householdTimezone.ts import { isValidIanaTimezone, getHouseholdTimezone } VERIFIED Line 30 of admin.ts: import confirmed
admin.ts PUT app_config db.insert(appConfig).values(...).onDuplicateKeyUpdate(...) VERIFIED Lines 227-231: upsert on household_timezone key
admin.ts seed app_config SELECT-before-conditional-INSERT with no-op onDuplicateKeyUpdate VERIFIED Lines 251-271: SELECT exists; INSERT only when !alreadySet; onDuplicateKeyUpdate preserves existing (WR-02)
reminderScheduler.ts householdTimezone.ts import { getHouseholdTimezone } + await getHouseholdTimezone(db) VERIFIED Import at line 51; call at line 250
outboxWorker.ts householdTimezone.ts import { getHouseholdTimezone } + two await getHouseholdTimezone(db) calls VERIFIED Import at line 46; calls at lines 504 and 612
AdminPage.tsx /api/admin/config/timezone useQuery(fetchAdminTimezone) + useMutation(setAdminTimezone) VERIFIED Lines 93 and 100 of AdminPage.tsx
client.ts PUT /api/admin/config/timezone fetch('/api/admin/config/timezone', { method: 'PUT', ... }) VERIFIED Line 502 of client.ts

Data-Flow Trace (Level 4)

Artifact Data Variable Source Produces Real Data Status
AdminPage.tsx Timezone section timezoneQuery.data?.timezone fetchAdminTimezone() → GET /api/admin/config/timezone → DB app_config SELECT Yes — admin.ts:201-212 performs real SELECT, returns stored or fallback FLOWING
reminderScheduler.ts all-day branch serverTz getHouseholdTimezone(db) → DB SELECT on app_config Yes — stored value or D-06 fallback FLOWING
outboxWorker.ts CREATE branch tz (line 504) getHouseholdTimezone(db) → DB SELECT Yes FLOWING
outboxWorker.ts UPDATE branch tz (line 612) getHouseholdTimezone(db) → DB SELECT Yes FLOWING

Behavioral Spot-Checks

Behavior Command Result Status
API test suite (372 tests) cd apps/api && npm test 372 passed (28 test files) PASS
PWA unit test suite (213 tests) cd apps/pwa && npm test 213 passed (18 test files) PASS
No bare process.env.TZ ?? Intl at broker call sites grep -c "process\.env\.TZ" reminderScheduler.ts 1 (comment only, line 249) PASS
No bare process.env.TZ ?? Intl at broker call sites grep -c "process\.env\.TZ" outboxWorker.ts 2 (comments only, lines 503, 611) PASS
getHouseholdTimezone(db) called in reminderScheduler grep -c "getHouseholdTimezone(db)" reminderScheduler.ts 1 PASS
getHouseholdTimezone(db) called in outboxWorker (both branches) grep -c "getHouseholdTimezone(db)" outboxWorker.ts 2 PASS
D-07: eventDateTime.ts not in phase diff git diff --name-only origin/main..HEAD -- apps/pwa/src/lib/eventDateTime.ts (empty) PASS
D-07: hydrateEvents.ts not in phase diff git diff --name-only origin/main..HEAD -- apps/pwa/src/lib/hydrateEvents.ts (empty) PASS

Requirements Coverage

Requirement Source Plan Description Status Evidence
D-01 18-02 Single household-wide timezone in app_config SATISFIED admin.ts:229 upserts household_timezone key
D-02 18-02, 18-04 Browser IANA timezone seeded at first run SATISFIED admin.ts:248-273 seed endpoint; AdminPage.tsx:108 detectedTz; POST seed called in UI
D-03 18-02 Seed never overwrites an explicit value SATISFIED SELECT-before-INSERT + onDuplicateKeyUpdate no-op; 4 tests
D-04 18-02, 18-04 Changeable via role-gated /admin Settings SATISFIED requireAdmin at line 42 covers all TZ routes; Timezone section in AdminPage.tsx
D-05 18-01, 18-03 Single shared accessor for broker TZ reads SATISFIED getHouseholdTimezone(db) is the only TZ read at all three broker sites; no duplication
D-06 18-01 Fallback chain: stored ?? TZ env ?? Intl SATISFIED householdTimezone.ts:34-38; 11 unit tests
D-07 18-03 Browser-local display/write path untouched SATISFIED Neither eventDateTime.ts nor hydrateEvents.ts in branch diff

Anti-Patterns Found

File Line Pattern Severity Impact
outboxWorker.ts 787 "not yet done" in log message Info Existing code; pre-dates this phase; contextual log text in business-logic comment, not a debt marker
outboxWorker.ts 827 "not yet applied" in comment Info Same as above; pre-existing natural-language description
AdminPage.tsx 392 placeholder="e.g. America/Chicago" Info Input placeholder text; not a stub implementation indicator

No TBD / FIXME / XXX / unreferenced debt markers found in any of the six Phase 18 production files.


Human Verification Required

1. Admin Timezone Picker Browser Round-Trip

Test: With the dev stack running and DEV_AUTH_BYPASS user flagged as admin, navigate to /admin. Observe the Timezone section.

Steps:

  1. Confirm "Timezone" section is visible, showing current household timezone (or "Using system default" note).
  2. Type a city (e.g. "Chicago"), pick "America/Chicago" from the datalist, click Save.
  3. Reload /admin and confirm the section shows "America/Chicago" with no "system default" note.
  4. Confirm the "Use detected: <zone>" affordance appears when the input does not match the browser zone.
  5. Click "Use detected: <zone>" — confirm it fills the input.
  6. Confirm save button is disabled when the input matches the stored value and the value is already explicit.
  7. Confirm save button is ENABLED when isExplicitlySet: false even if the input matches the displayed default (WR-01 fix).

Expected: Value persists across reload; no console errors; save button disables correctly; first-run scenario allows saving the displayed default to make it explicit.

Why human: Playwright e2e spec (timezone-verify.spec.ts, 6 tests) was run against the live stack at execution time and passed. Re-running requires the full Docker stack with DEV_AUTH_BYPASS=true and an admin-flagged user, which cannot be driven from this verification process. iOS/Safari behavior is also out of scope for playwright-cli.


Gaps Summary

No automated gaps found. All seven decision-contract truths are VERIFIED against the codebase.

Post-review fixes applied and confirmed:

  • WR-01 (173e06e): timezoneSaveDisabled now allows saving when isExplicitlySet: false even if the input matches the displayed default. Unit tests at AdminPage.timezone.test.ts:44,57 cover this.
  • WR-02 (bda31a3): Seed endpoint uses onDuplicateKeyUpdate({ set: { value: sql\value` } })to prevent duplicate-key 500 under concurrent race. Four tests inadmin.test.ts:711-806` cover the 403, seeded:true, no-overwrite, and idempotent-race cases.

Accepted review item:

  • WR-03 (GET handler issues a second DB round-trip on unset path via getHouseholdTimezone(db) after already reading the row): accepted in 18-REVIEW.md as a minor inefficiency, not a correctness issue. No impact on goal achievement.

The only pending item is the human browser verification of the admin timezone picker UI (Task 3 of Plan 18-04), which was executed and passed at phase execution time but cannot be re-run without the live stack.


Verified: 2026-06-14T23:05:00Z Verifier: Claude (gsd-verifier)