--- phase: 18-auto-timezone-detection-and-ability-to-change-timezone plan: "02" subsystem: api tags: [timezone, iana, drizzle, vitest, tdd, admin, hono] # Dependency graph requires: - phase: 18-01 provides: isValidIanaTimezone + getHouseholdTimezone (consumed by timezoneSchema + GET handler) - phase: 10-admin-role-settings provides: adminRouter + requireAdmin + appConfig table provides: - GET /api/admin/config/timezone — reads stored household timezone with isExplicitlySet flag - PUT /api/admin/config/timezone — validates IANA string + upserts household_timezone in app_config - POST /api/admin/config/timezone/seed — seeds only when unset (D-03 no-overwrite) affects: - 18-03-PLAN (broker rewire — reminderScheduler + outboxWorker import getHouseholdTimezone) - 18-04-PLAN (PWA settings UI — consumes GET + PUT endpoints built here) # Tech tracking tech-stack: added: [] patterns: - zValidator('json', schema) without noEchoHook for non-sensitive config (timezone is not a credential) - Drizzle onDuplicateKeyUpdate upsert for config PUT (appConfig PK = key) - SELECT-before-INSERT pattern for no-overwrite seed (D-03 — DO NOT use onDuplicateKeyUpdate for seed) - requireAdmin positional coverage: new routes appended after line-41 adminRouter.use('*', requireAdmin) key-files: created: [] modified: - apps/api/src/routes/admin.ts - apps/api/tests/routes/admin.test.ts key-decisions: - "timezoneSchema uses isValidIanaTimezone (from 18-01) in Zod .refine() — no noEchoHook needed (T-18-06: timezone is non-sensitive)" - "GET /config/timezone reads row directly (not via getHouseholdTimezone) to compute isExplicitlySet from row?.value != null, then uses getHouseholdTimezone for the fallback value" - "seed endpoint uses SELECT-then-INSERT (not onDuplicateKeyUpdate) to ensure D-03 no-overwrite is an explicit code path, not a silent race" requirements-completed: [D-01, D-02, D-03, D-04] # Metrics duration: 3min completed: 2026-06-15 --- # Phase 18 Plan 02: Admin Timezone API Endpoints Summary **Three role-gated admin endpoints (GET + PUT + seed) added to adminRouter with server-side IANA validation via isValidIanaTimezone and D-03 no-overwrite seed semantics enforced by SELECT-before-INSERT** ## Performance - **Duration:** 3 min - **Started:** 2026-06-15T02:09:38Z - **Completed:** 2026-06-15T02:12:39Z - **Tasks:** 2 (TDD RED + GREEN) - **Files modified:** 2 ## Accomplishments - Added `describe('admin timezone config')` to `admin.test.ts` with 8 test cases covering all boundary conditions - Implemented `GET /api/admin/config/timezone`, `PUT /api/admin/config/timezone`, and `POST /api/admin/config/timezone/seed` on the existing `adminRouter` - All routes inherit the line-41 `requireAdmin` guard (Pitfall 9 / T-18-03) — no per-route auth addition needed - `timezoneSchema` uses `isValidIanaTimezone` from Plan 18-01 in a Zod `.refine()` — no `noEchoHook` (T-18-06) - `PUT` uses `onDuplicateKeyUpdate` for a true upsert; `seed` uses SELECT-then-INSERT to enforce D-03 no-overwrite - 25/25 `admin.test.ts` tests pass; 366/366 full API suite green; `tsc --noEmit` clean ## Task Commits 1. **Task 1: RED — failing integration tests** - `f109b3c` (test) 2. **Task 2: GREEN — implement GET/PUT/seed timezone endpoints** - `3bd6a5d` (feat) ## Files Created/Modified - `apps/api/src/routes/admin.ts` — added appConfig + householdTimezone imports, timezoneSchema, and three new route handlers - `apps/api/tests/routes/admin.test.ts` — added appConfig import + `describe('admin timezone config')` with 8 test cases + per-test afterEach cleanup ## Decisions Made - `timezoneSchema` does NOT use `noEchoHook` because timezone strings are non-sensitive (not credentials/PII); standard `zValidator` error responses are safe (T-18-06 accepted disposition). - `GET /config/timezone` does a direct single-row read (not `getHouseholdTimezone`) so the handler can compute `isExplicitlySet` from `row?.value != null` before deciding whether to invoke the fallback chain — using `getHouseholdTimezone` would discard the "was it stored?" signal. - `POST /api/admin/config/timezone/seed` uses a SELECT-then-INSERT (not `onDuplicateKeyUpdate`) so that D-03 no-overwrite is an explicit code branch, not an implicit race. The test asserts the stored value after a second seed attempt remains unchanged. ## Deviations from Plan None — plan executed exactly as written. ## Issues Encountered - Running `pnpm --filter @familysync/api exec vitest run` requires `DB_ROOT_PASSWORD` from `.env` sourced into the shell (the global-setup provisions `familysync_test` using the root credential). This is the established pattern from quick 260613-ndv and documented in `familysync-dev-stack-setup.md`. ## Known Stubs None — all three endpoints are fully wired to the database. No placeholder data. ## Threat Flags No new threat surface beyond the plan's threat model. All three endpoints are behind `requireAdmin` (T-18-03). IANA validation enforced by `isValidIanaTimezone` (T-18-04). No SQL injection surface — key is a hard-coded literal, value is IANA-validated, Drizzle parameterizes the insert/upsert (T-18-07). ## TDD Gate Compliance - RED gate: `f109b3c` — `test(18-02): add failing integration tests for admin timezone endpoints` - GREEN gate: `3bd6a5d` — `feat(18-02): admin timezone GET/PUT/seed endpoints` - REFACTOR gate: N/A (implementation was clean on first pass) ## Next Phase Readiness - Plan 18-03 (broker rewire) can import `getHouseholdTimezone(db)` from `../lib/householdTimezone.js` to replace the bare `process.env.TZ ?? Intl…` lookups in `reminderScheduler.ts:247` and `outboxWorker.ts:501,607`. - Plan 18-04 (PWA settings UI) can wire to `GET /api/admin/config/timezone` and `PUT /api/admin/config/timezone`. The seed endpoint is also available for the Phase 12 wizard auto-detect flow. ## Self-Check: PASSED - `apps/api/src/routes/admin.ts` — FOUND (modified) - `apps/api/tests/routes/admin.test.ts` — FOUND (modified) - Commit `f109b3c` — FOUND - Commit `3bd6a5d` — FOUND --- *Phase: 18-auto-timezone-detection-and-ability-to-change-timezone* *Completed: 2026-06-15*