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

174 lines
8.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` §~233315 — `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*