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

6.1 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, requirements-completed, duration, completed
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions requirements-completed duration completed
18-auto-timezone-detection-and-ability-to-change-timezone 02 api
timezone
iana
drizzle
vitest
tdd
admin
hono
phase provides
18-01 isValidIanaTimezone + getHouseholdTimezone (consumed by timezoneSchema + GET handler)
phase provides
10-admin-role-settings adminRouter + requireAdmin + appConfig table
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)
18-03-PLAN (broker rewire — reminderScheduler + outboxWorker import getHouseholdTimezone)
18-04-PLAN (PWA settings UI — consumes GET + PUT endpoints built here)
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)
created modified
apps/api/src/routes/admin.ts
apps/api/tests/routes/admin.test.ts
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
D-01
D-02
D-03
D-04
3min 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: f109b3ctest(18-02): add failing integration tests for admin timezone endpoints
  • GREEN gate: 3bd6a5dfeat(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