diff --git a/.planning/phases/18-auto-timezone-detection-and-ability-to-change-timezone/18-CONTEXT.md b/.planning/phases/18-auto-timezone-detection-and-ability-to-change-timezone/18-CONTEXT.md new file mode 100644 index 0000000..44f8942 --- /dev/null +++ b/.planning/phases/18-auto-timezone-detection-and-ability-to-change-timezone/18-CONTEXT.md @@ -0,0 +1,173 @@ +# Phase 18: Auto timezone detection and ability to change timezone - Context + +**Gathered:** 2026-06-14 +**Status:** Ready for planning + + +## Phase Boundary + +Make the household timezone an **explicit, stored, user-changeable setting** — +auto-detected from the browser — instead of the implicit `process.env.TZ` +fallback that all-day reminder computation relies on today. + +Today the timezone is invisible: the server uses +`process.env.TZ ?? Intl.DateTimeFormat().resolvedOptions().timeZone` (which is +UTC inside Docker) purely for the all-day "9 AM local" reminder computation, +while the browser uses its own local zone for write serialization and display. +This phase stores a single household timezone, seeds it from the browser at +setup, lets an admin change it, and routes the server-side all-day reminder +computation through it. + +**In scope:** +- Store a single household timezone (new `app_config` key, e.g. `household_timezone`). +- Auto-detect the browser IANA timezone and seed the stored value during the + Phase 12 setup wizard / first run. +- Admin-facing UI to view and change the household timezone (searchable IANA picker). +- Route the server-side **all-day reminder** "9 AM local" computation through the + stored timezone (overriding the `process.env.TZ` fallback when set). + +**Out of scope:** +- Per-member timezones (single household value only — see D-01). +- Auto-overwrite of the stored timezone on login/travel/VPN drift (see D-03). +- Changing display rendering or timed-event serialization, which stay + browser-local and already-correct (see D-07). + + + + +## Implementation Decisions + +### Timezone scope +- **D-01:** Single **household-wide** timezone, stored in `app_config` (one row, + e.g. key `household_timezone`, IANA string value). Chosen over a per-member + `users.timezone` column — the household is two people in the same location, and + a single value matches the existing implicit single-TZ model. No new column or + per-row scheduler logic. + +### Detection & drift +- **D-02:** Auto-detect the browser IANA timezone + (`Intl.DateTimeFormat().resolvedOptions().timeZone`) and use it to **seed** the + stored value during the Phase 12 setup wizard / first run. +- **D-03:** After seeding, the timezone changes **only** via the settings UI. Do + NOT auto-overwrite the stored value on later login/detection differences + (travel, VPN, new device). Silent drift would unexpectedly shift the "fires at + 9 AM" guarantee. (Optional drift *notice* is allowed but not required — never an + automatic write.) + +### Where to change it +- **D-04:** Surface the timezone in the existing **role-gated `/admin` Settings** + (Phase 10 `requireAdmin` boundary) and seed it from the **Phase 12 setup + wizard**. It is a single household-wide setting, so admin-gating is the correct + fit; server-side `requireAdmin` is the real boundary (client `isAdmin` is UX + only, per Phase 10 D-03). + +### Behavioral boundary +- **D-05:** The stored timezone becomes the **source of truth for the + server-side all-day "9 AM local" reminder computation** + (`reminderScheduler.ts`, `outboxWorker.ts`), replacing the bare + `process.env.TZ ?? Intl…` lookup at those sites. +- **D-06:** **Fallback chain when `household_timezone` is unset** (e.g. before the + wizard seeds it, or on upgrade of an existing install): fall back to the + current behavior — `process.env.TZ ?? Intl.DateTimeFormat().resolvedOptions().timeZone`. + Nothing breaks before a value is stored; this is the backward-compat path. +- **D-07:** Display rendering and timed-event write serialization stay + **browser-local** and unchanged. This phase must NOT touch the already-correct + browser-local write/display path (`eventDateTime.ts`, `hydrateEvents.ts`) — + keep the blast radius to the all-day reminder computation only. + +### Claude's Discretion +- Timezone picker UX: a **searchable IANA dropdown** (recorded default; planner/UI + may refine). Validate the value is a real IANA zone before storing. +- Exact `app_config` key name and the read/cache strategy for the stored value in + the scheduler/outbox (e.g. read-per-run vs cached) — planner's call. +- Whether to show a non-blocking "detected zone differs" notice on login (allowed + per D-03, not required). + + + + +## Canonical References + +**Downstream agents MUST read these before planning or implementing.** + +### Current timezone touchpoints (the code this phase rewires) +- `apps/api/src/broker/reminderScheduler.ts` §~247 — `serverTz` lookup for the + all-day branch; primary site to route through the stored timezone (D-05). +- `apps/api/src/broker/outboxWorker.ts` §~501, §~607 — `tz` lookups feeding + `computeAlertInstantUtc` for all-day alert instants; same rewire (D-05). +- `apps/api/src/broker/vevent.ts` §~233–315 — `computeAlertInstantUtc`: the + pure 9 AM-local computation that *consumes* the tz; contract unchanged, only + the tz it's fed changes. + +### Storage & settings surfaces +- `apps/api/src/db/schema.ts` §282 — `appConfig` table (key/value/updatedAt); + where the `household_timezone` key lives (D-01). +- Phase 10 admin surface — `apps/api/src/` `/api/admin/*` routes + + `requireAdmin`, and the PWA `/admin` Settings route; where the change-tz UI + attaches (D-04). See `.planning/phases/10-admin-role-settings/10-CONTEXT.md`. +- Phase 12 setup wizard — `.planning/phases/12-*/12-CONTEXT.md` and its + `12-UI-SPEC.md`; where detection seeding hooks in (D-02). **Note dependency + ordering:** Phase 12 may not be executed yet — planner should confirm and, if + needed, make wizard seeding additive/optional so Phase 18 isn't blocked. + +### Browser-local path (do NOT modify — boundary guard) +- `apps/pwa/src/lib/eventDateTime.ts` — browser-local write serialization (D-07). +- `apps/pwa/src/lib/hydrateEvents.ts` — display hydration (D-07). + + + + +## Existing Code Insights + +### Reusable Assets +- `appConfig` table (`schema.ts:282`) — already exists from Phase 10; store the + household timezone as a single key/value row, no migration needed for a new key + (only seed/read logic). +- `requireAdmin` gating + `/admin` Settings route (Phase 10) — reuse directly for + the change-tz control; no new auth surface. +- Setup wizard scaffolding (Phase 12) — reuse for first-run detection + seeding. +- `Intl.DateTimeFormat().resolvedOptions().timeZone` — already the detection + primitive used server-side; reuse client-side for seeding. + +### Established Patterns +- Server is the real authorization boundary; client role flags are UX only + (Phase 10 D-03) — apply to any tz-write endpoint. +- All-day reminder math is isolated in `computeAlertInstantUtc` (pure, tz-arg) — + the phase only needs to change *what tz is passed*, not the math. +- Backward-compat via additive `app_config` keys + fallback defaults (Phase 10 / + Phase 11 style) — keep the `process.env.TZ ?? Intl` fallback (D-06). + +### Integration Points +- All-day reminder fire path: `reminderScheduler.ts` and `outboxWorker.ts` both + independently read tz — both must route through the same stored-value accessor + (single helper to avoid drift between the two sites). +- Admin Settings (read/write tz) ↔ `app_config` ↔ scheduler/outbox read path. + + + + +## Specific Ideas + +- The "9 AM local" all-day reminder guarantee is the load-bearing reason this + setting exists — correctness of that fire time is the success measure. +- Keep the change tightly scoped to the all-day reminder computation; the + browser-local timed/display path was deliberately fixed earlier and must not + regress. + + + + +## Deferred Ideas + +- **Per-member timezones** — only relevant if household members live in different + zones; revisit as its own phase if that ever becomes true (would add + `users.timezone` + per-row scheduler logic). +- **Driving display/timed reminders off the stored tz** — broader behavior change + deliberately excluded (D-07) to protect the already-correct browser-local path. + + + +--- + +*Phase: 18-auto-timezone-detection-and-ability-to-change-timezone* +*Context gathered: 2026-06-14* diff --git a/.planning/phases/18-auto-timezone-detection-and-ability-to-change-timezone/18-DISCUSSION-LOG.md b/.planning/phases/18-auto-timezone-detection-and-ability-to-change-timezone/18-DISCUSSION-LOG.md new file mode 100644 index 0000000..bd921c2 --- /dev/null +++ b/.planning/phases/18-auto-timezone-detection-and-ability-to-change-timezone/18-DISCUSSION-LOG.md @@ -0,0 +1,70 @@ +# Phase 18: Auto timezone detection and ability to change timezone - Discussion Log + +> **Audit trail only.** Do not use as input to planning, research, or execution agents. +> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered. + +**Date:** 2026-06-14 +**Phase:** 18-auto-timezone-detection-and-ability-to-change-timezone +**Areas discussed:** Timezone scope, Detection & drift, Where to change it, Behavioral boundary + +--- + +## Timezone scope + +| Option | Description | Selected | +|--------|-------------|----------| +| Single household tz | One value in app_config; matches existing implicit single-TZ model and the 2-person same-household reality | ✓ | +| Per-member timezone | users.timezone column; per-member 9 AM reminders; needs migration + per-row scheduler logic | | + +**User's choice:** Single household tz (app_config) +**Notes:** Two-person same household — single value is the simplest correct model. + +--- + +## Detection & drift + +| Option | Description | Selected | +|--------|-------------|----------| +| Seed on setup, manual after | Auto-detect during Phase 12 wizard/first run to seed; thereafter changes only via settings UI, no auto-overwrite | ✓ | +| Detect every login, prompt on drift | Compare browser tz to stored on each login; prompt to update when different | | +| Detect every login, silent update | Always overwrite stored tz with the browser's | | + +**User's choice:** Seed on setup, manual after +**Notes:** Avoids travel/VPN silently shifting the "fires at 9 AM" guarantee. + +--- + +## Where to change it + +| Option | Description | Selected | +|--------|-------------|----------| +| Admin Settings + wizard | Role-gated /admin Settings (Phase 10) + seeded by the Phase 12 setup wizard | ✓ | +| Any member, general settings | Non-admin settings area; fits per-member scope better | | + +**User's choice:** Admin Settings + wizard +**Notes:** Single household-wide setting behind requireAdmin is the correct fit. + +--- + +## Behavioral boundary + +| Option | Description | Selected | +|--------|-------------|----------| +| All-day reminder tz only | Stored tz is source of truth for server-side all-day 9 AM computation, overriding process.env.TZ; display + timed serialization stay browser-local | ✓ | +| Also override display / timed logic | Stored tz also drives display rendering and/or timed reminders | | + +**User's choice:** All-day reminder tz only +**Notes:** Keeps blast radius small; protects the already-correct browser-local write/display path. + +--- + +## Claude's Discretion + +- Timezone picker UX (recorded default: searchable IANA dropdown; validate IANA before storing). +- Exact app_config key name + read/cache strategy for the stored value in scheduler/outbox. +- Optional non-blocking "detected zone differs" login notice (allowed, not required). + +## Deferred Ideas + +- Per-member timezones — only relevant if members live in different zones; own phase if ever true. +- Driving display/timed reminders off the stored tz — deliberately excluded to protect the browser-local path.