Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
174 lines
8.0 KiB
Markdown
174 lines
8.0 KiB
Markdown
# Phase 18: Auto timezone detection and ability to change timezone - Context
|
||
|
||
**Gathered:** 2026-06-14
|
||
**Status:** Ready for planning
|
||
|
||
<domain>
|
||
## 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).
|
||
|
||
</domain>
|
||
|
||
<decisions>
|
||
## 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).
|
||
|
||
</decisions>
|
||
|
||
<canonical_refs>
|
||
## 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).
|
||
|
||
</canonical_refs>
|
||
|
||
<code_context>
|
||
## 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.
|
||
|
||
</code_context>
|
||
|
||
<specifics>
|
||
## 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.
|
||
|
||
</specifics>
|
||
|
||
<deferred>
|
||
## 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.
|
||
|
||
</deferred>
|
||
|
||
---
|
||
|
||
*Phase: 18-auto-timezone-detection-and-ability-to-change-timezone*
|
||
*Context gathered: 2026-06-14*
|