docs(18): verification passed + mark phase complete
Phase-goal verification: 7/7 decision-contract truths confirmed (D-01..D-07). Browser round-trip re-confirmed via Playwright e2e against the live stack. Mark phase 18 complete in STATE.md and ROADMAP.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
ddeb87cdba
commit
ea93089b74
@@ -415,6 +415,7 @@ At ≤767px (`window.matchMedia('(max-width: 767px)')` in `apps/pwa/src/App.tsx`
|
||||
| 15. Doc-Only CI Skip + MD Lint | v1.1 | 3/3 | Complete | 2026-06-12 |
|
||||
| 16. CI Dep Audit, Sec & Img Hyg | v1.1 | 6/6 | Complete | 2026-06-13 |
|
||||
| 17. UI Optimization & Polish | v1.1 | 0/? | Not started | - |
|
||||
| 18. Auto Timezone Detection | v1.1 | 4/4 | Complete | 2026-06-14 |
|
||||
|
||||
## Backlog
|
||||
|
||||
|
||||
+9
-9
@@ -2,16 +2,16 @@
|
||||
gsd_state_version: 1.0
|
||||
milestone: v1.1
|
||||
milestone_name: Operability & Polish
|
||||
status: verifying
|
||||
status: completed
|
||||
stopped_at: Phase 18 Plan 03 complete — broker rewire done; plan 4 of 4 is next
|
||||
last_updated: "2026-06-15T02:46:09.819Z"
|
||||
last_activity: 2026-06-15
|
||||
last_updated: "2026-06-15T03:08:19.109Z"
|
||||
last_activity: 2026-06-15 -- Phase 18 marked complete
|
||||
progress:
|
||||
total_phases: 23
|
||||
completed_phases: 10
|
||||
total_plans: 36
|
||||
completed_phases: 9
|
||||
total_plans: 37
|
||||
completed_plans: 36
|
||||
percent: 43
|
||||
percent: 39
|
||||
---
|
||||
|
||||
# Project State
|
||||
@@ -25,10 +25,10 @@ See: .planning/PROJECT.md (updated 2026-06-10)
|
||||
|
||||
## Current Position
|
||||
|
||||
Phase: 18 (auto-timezone-detection-and-ability-to-change-timezone) — EXECUTING
|
||||
Phase: 18 — COMPLETE
|
||||
Plan: 4 of 4
|
||||
Status: Phase complete — ready for verification
|
||||
Last activity: 2026-06-15
|
||||
Status: Phase 18 complete
|
||||
Last activity: 2026-06-15 -- Phase 18 marked complete
|
||||
|
||||
### ✅ Resolved Checkpoint — Phase 15 Plan 15-03 Task 2 (human-action)
|
||||
|
||||
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
---
|
||||
phase: 18-auto-timezone-detection-and-ability-to-change-timezone
|
||||
verified: 2026-06-14T23:05:00Z
|
||||
status: passed
|
||||
score: 7/7
|
||||
overrides_applied: 0
|
||||
browser_verification:
|
||||
- test: "Admin timezone picker round-trip in browser"
|
||||
result: "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."
|
||||
note: "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:229` — `db.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:108` — `detectedTz = 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 at `admin.test.ts:753-806` confirm no-overwrite and idempotency under race. |
|
||||
| 4 | D-04 | Timezone changeable from role-gated /admin only | VERIFIED | `admin.ts:42` — `adminRouter.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-38` — `row?.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. |
|
||||
|
||||
---
|
||||
|
||||
### Key Link Verification
|
||||
|
||||
| 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 in `admin.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)_
|
||||
Reference in New Issue
Block a user