From 08c39165bfbf2adbe588918cc03b7259b6ae83bd Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Sun, 14 Jun 2026 22:13:53 -0400 Subject: [PATCH] docs(18-02): complete admin timezone endpoints plan --- .planning/ROADMAP.md | 4 +- .planning/STATE.md | 9 +- .../18-02-SUMMARY.md | 123 ++++++++++++++++++ 3 files changed, 130 insertions(+), 6 deletions(-) create mode 100644 .planning/phases/18-auto-timezone-detection-and-ability-to-change-timezone/18-02-SUMMARY.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 12247f3..0c5a608 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -636,7 +636,7 @@ Plans: **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. **Requirements**: TBD (decision contract D-01..D-07 from 18-CONTEXT.md) **Depends on:** Phase 10 (admin role + `/admin` Settings + `app_config`); Phase 11 (all-day reminder computation this rewires). Independent of Phase 17. Phase 12 (setup wizard) not required — seeding is self-contained. -**Plans:** 1/4 plans executed +**Plans:** 2/4 plans executed Plans: **Wave 1** @@ -645,7 +645,7 @@ Plans: **Wave 2** *(blocked on Wave 1 completion)* -- [ ] 18-02-PLAN.md — TDD: admin GET/PUT/seed timezone endpoints on adminRouter, requireAdmin + IANA validation + no-overwrite seed (D-01/D-02/D-03/D-04) +- [x] 18-02-PLAN.md — TDD: admin GET/PUT/seed timezone endpoints on adminRouter, requireAdmin + IANA validation + no-overwrite seed (D-01/D-02/D-03/D-04) - [ ] 18-03-PLAN.md — TDD: route all-day reminder TZ at reminderScheduler:247 + outboxWorker:501,607 through the accessor (D-05/D-06/D-07) **Wave 3** *(blocked on Wave 2 completion)* diff --git a/.planning/STATE.md b/.planning/STATE.md index 74f1da6..59cf015 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -4,13 +4,13 @@ milestone: v1.1 milestone_name: Operability & Polish status: executing stopped_at: Phase 18 context gathered -last_updated: "2026-06-15T02:08:26.245Z" +last_updated: "2026-06-15T02:13:43.020Z" last_activity: 2026-06-15 progress: total_phases: 23 completed_phases: 9 total_plans: 36 - completed_plans: 33 + completed_plans: 34 percent: 39 --- @@ -26,7 +26,7 @@ See: .planning/PROJECT.md (updated 2026-06-10) ## Current Position Phase: 18 (auto-timezone-detection-and-ability-to-change-timezone) — EXECUTING -Plan: 2 of 4 +Plan: 3 of 4 Status: Ready to execute Last activity: 2026-06-15 @@ -108,6 +108,7 @@ _Updated after each plan completion_ | Phase 10-admin-role-settings P04 | 1315 | 3 tasks | 8 files | | Phase 11-per-event-reminders P11-04 | 60 | 3 tasks | 4 files | | Phase 18-auto-timezone-detection-and-ability-to-change-timezone P01 | 2 | 2 tasks | 2 files | +| Phase 18 P02 | 3 | 2 tasks | 2 files | ## Accumulated Context @@ -253,7 +254,7 @@ Recent decisions affecting current work: ## Session Continuity -Last session: 2026-06-15T02:08:26.229Z +Last session: 2026-06-15T02:13:43.000Z Stopped at: Phase 18 context gathered Resume file: None diff --git a/.planning/phases/18-auto-timezone-detection-and-ability-to-change-timezone/18-02-SUMMARY.md b/.planning/phases/18-auto-timezone-detection-and-ability-to-change-timezone/18-02-SUMMARY.md new file mode 100644 index 0000000..160fa63 --- /dev/null +++ b/.planning/phases/18-auto-timezone-detection-and-ability-to-change-timezone/18-02-SUMMARY.md @@ -0,0 +1,123 @@ +--- +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*