Files
familysync/.planning/phases/18-auto-timezone-detection-and-ability-to-change-timezone/18-03-PLAN.md
T

12 KiB
Raw Blame History

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
18-auto-timezone-detection-and-ability-to-change-timezone 03 tdd 2
18-01
apps/api/src/broker/reminderScheduler.ts
apps/api/src/broker/outboxWorker.ts
apps/api/tests/broker/reminderScheduler.test.ts
apps/api/tests/broker/outboxWorker.test.ts
true
D-05
D-06
D-07
truths artifacts key_links
reminderScheduler.ts all-day branch computes the 9 AM-local alert instant using the stored household_timezone when set (D-05)
outboxWorker.ts both all-day branches (create + update) compute the alert instant using the stored household_timezone when set (D-05)
When household_timezone is unset, all three sites fall back to process.env.TZ ?? Intl — existing all-day tests pass unmodified (D-06)
Both files route through the single getHouseholdTimezone accessor — the read/fallback logic is NOT duplicated at the call sites (D-05)
path provides
apps/api/src/broker/reminderScheduler.ts all-day TZ now sourced from getHouseholdTimezone(db) (line ~247 rewire)
path provides
apps/api/src/broker/outboxWorker.ts all-day TZ now sourced from getHouseholdTimezone(db) (lines ~501, ~607 rewire)
from to via pattern
apps/api/src/broker/reminderScheduler.ts apps/api/src/lib/householdTimezone.ts import { getHouseholdTimezone } getHouseholdTimezone(db)
from to via pattern
apps/api/src/broker/outboxWorker.ts apps/api/src/lib/householdTimezone.ts import { getHouseholdTimezone } getHouseholdTimezone(db)
Rewire the three all-day "9 AM local" timezone lookups — one in `reminderScheduler.ts` (line ~247) and two in `outboxWorker.ts` (lines ~501 and ~607) — to read the stored timezone through the single `getHouseholdTimezone(db)` accessor, replacing the bare `process.env.TZ ?? Intl.DateTimeFormat().resolvedOptions().timeZone` reads.

Purpose: D-05 makes the stored household_timezone the source of truth for the all-day reminder computation, and mandates ONE shared accessor at both sites (no duplicated read/fallback). D-06 keeps the exact process.env.TZ ?? Intl behavior when nothing is stored, so the existing all-day tests pass unmodified. D-07 is the hard boundary: this plan must NOT touch display/timed-write paths. Output: three rewired call sites + new stored-TZ tests; existing all-day tests green unchanged.

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/18-auto-timezone-detection-and-ability-to-change-timezone/18-CONTEXT.md @.planning/phases/18-auto-timezone-detection-and-ability-to-change-timezone/18-RESEARCH.md @.planning/phases/18-auto-timezone-detection-and-ability-to-change-timezone/18-PATTERNS.md @apps/api/src/lib/householdTimezone.ts Task 1: RED — failing stored-TZ all-day tests for scheduler + outbox apps/api/tests/broker/reminderScheduler.test.ts, apps/api/tests/broker/outboxWorker.test.ts - apps/api/tests/broker/reminderScheduler.test.ts (lines ~656729: existing all-day tests that pin process.env.TZ='America/New_York' — DO NOT modify them; they must keep passing via D-06 fallback) - apps/api/tests/broker/outboxWorker.test.ts (how its mock db chain is built for the all-day branches) - apps/api/src/broker/vevent.ts §240 (computeAlertInstantUtc(eventDateStr, leadDays, tz): Date — contract unchanged; assert the 9 AM-local instant for the stored zone) - apps/api/src/lib/householdTimezone.ts (the accessor whose stored-value path you are now exercising) - .planning/phases/18-auto-timezone-detection-and-ability-to-change-timezone/18-RESEARCH.md (Pitfall 5: why existing tests stay green; new tests mock db to return the app_config row) - reminderScheduler all-day: when the mock db ALSO returns { key: 'household_timezone', value: 'America/Chicago' }, the computed alert instant equals 9 AM America/Chicago for the event date (a different UTC instant than 9 AM America/New_York would give). - outboxWorker all-day (create branch): with stored 'America/Chicago', the create-branch alert instant is 9 AM Chicago for the event date. - outboxWorker all-day (update branch): with stored 'America/Chicago', the update-branch alert instant is 9 AM Chicago for the event date. - Backward-compat (assert, do not modify): the pre-existing all-day tests that pin process.env.TZ and return NO app_config row still pass (fallback fires). Add new stored-TZ test cases alongside the existing all-day tests. Where the existing tests mock the Drizzle query for events, extend the mock so the household_timezone SELECT resolves to a row with value 'America/Chicago' (match the accessor's query shape so getHouseholdTimezone returns it). Assert the resulting UTC instant equals 9 AM Chicago for the event date (compute the expected UTC via a fixed reference, e.g. using computeAlertInstantUtc with 'America/Chicago' directly, or an explicit ISO instant). Do NOT alter the existing process.env.TZ-pinned cases. Run the two suites; confirm the NEW cases FAIL (code still reads process.env.TZ, so stored 'America/Chicago' has no effect). Commit as `test(18-03): add failing stored-TZ all-day tests for scheduler + outbox`. pnpm --filter @familysync/api exec vitest run tests/broker/reminderScheduler.test.ts tests/broker/outboxWorker.test.ts 2>&1 | grep -qi "fail" && echo RED-OK - New stored-TZ cases exist in both test files and FAIL before the rewire (code still reads process.env.TZ). - The existing process.env.TZ-pinned all-day cases are unchanged (no edits to those test bodies). - `test(18-03): ...` commit precedes the rewire commit. RED gate met: stored-TZ tests fail because the call sites are not yet rewired. Task 2: GREEN — rewire all three all-day TZ call sites through getHouseholdTimezone(db) apps/api/src/broker/reminderScheduler.ts, apps/api/src/broker/outboxWorker.ts - apps/api/src/broker/reminderScheduler.ts (line ~247 serverTz lookup; line ~256 computeAlertInstantUtc call; db already imported line 46) - apps/api/src/broker/outboxWorker.ts (lines ~501 + ~607 tz lookups; runOutboxDrain line ~710; db already imported line 31) - apps/api/src/lib/householdTimezone.ts (import getHouseholdTimezone) - .planning/phases/18-auto-timezone-detection-and-ability-to-change-timezone/18-PATTERNS.md (reminderScheduler + outboxWorker sections: exact replacement lines) In `reminderScheduler.ts`: add `import { getHouseholdTimezone } from '../lib/householdTimezone.js'` and replace the line-247 `const serverTz = process.env.TZ ?? Intl…` with `const serverTz = await getHouseholdTimezone(db);` (the enclosing function is already async / awaits DB queries; `db` is the module import on line 46). Leave the line-256 computeAlertInstantUtc call unchanged — serverTz is still a string. In `outboxWorker.ts`: add the same import. Replace BOTH bare `const tz = process.env.TZ ?? Intl…` lookups (~501 and ~607) with `const tz = await getHouseholdTimezone(db);`. `db` is the module import on line 31; `runOutboxDrain` is async. (If both branches are reachable in one pass and lint allows, you may hoist a single `const tz = await getHouseholdTimezone(db)` to the top of the all-day block and reuse it — but do NOT duplicate the read/fallback inline; route through the accessor only.) HARD BOUNDARY (D-07): do not open, import from, or modify `apps/pwa/src/lib/eventDateTime.ts` or `apps/pwa/src/lib/hydrateEvents.ts`, and do not change any timed-event serialization or display path. Run both suites; iterate to GREEN. Confirm the existing process.env.TZ-pinned tests still pass. Commit as `feat(18-03): route all-day reminder TZ through stored household_timezone`. pnpm --filter @familysync/api exec vitest run tests/broker/reminderScheduler.test.ts tests/broker/outboxWorker.test.ts - `grep -c "getHouseholdTimezone(db)" apps/api/src/broker/reminderScheduler.ts` ≥ 1 and same for outboxWorker.ts ≥ 1. - No bare `process.env.TZ ?? Intl.DateTimeFormat().resolvedOptions().timeZone` remains at the rewired all-day sites (assert: `grep -v '^\s*\*' apps/api/src/broker/reminderScheduler.ts | grep -c "process.env.TZ ?? Intl"` → 0; same for outboxWorker.ts). The fallback now lives only inside getHouseholdTimezone. - D-07 boundary: `git diff --name-only` for this plan does NOT include `apps/pwa/src/lib/eventDateTime.ts` or `apps/pwa/src/lib/hydrateEvents.ts` (assert they are absent from the changed-files list). - New stored-TZ tests GREEN; existing process.env.TZ all-day tests still GREEN (unmodified). - `pnpm --filter @familysync/api test -- --run` fully green; `tsc --noEmit` clean. - `feat(18-03): ...` commit follows the RED commit. GREEN gate met: stored TZ drives all three all-day sites; D-06 fallback + D-07 boundary intact.

<threat_model>

Trust Boundaries

Boundary Description
scheduler/outbox → DB trusted server worker reads the validated stored timezone to compute fire times

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-18-08 Tampering stored TZ fed to computeAlertInstantUtc could be garbage mitigate The value can only have been written via Plan 02's IANA-validated PUT/seed; the accessor returns either a validated stored string or the D-06 fallback. No unvalidated value reaches computeAlertInstantUtc.
T-18-09 Tampering (regression) accidental change to display/timed-write path mitigate D-07 boundary enforced as an acceptance criterion: changed-files list must exclude eventDateTime.ts + hydrateEvents.ts; no timed-event serialization touched. Blast radius confined to the all-day branches.
T-18-10 Denial of Service extra DB read per tick at 3 call sites accept PK lookups on a 60s interval; negligible (RESEARCH A1). Read-per-run keeps changes propagating within one tick without a worker restart.
T-18-SC Tampering npm/pip/cargo installs mitigate No new packages this plan (RESEARCH Package Legitimacy Audit: zero installs).
</threat_model>
- `pnpm --filter @familysync/api exec vitest run tests/broker/reminderScheduler.test.ts tests/broker/outboxWorker.test.ts` green. - `pnpm --filter @familysync/api test -- --run` green (all 244+ existing tests, including unmodified all-day cases). - D-07: changed-files exclude the two PWA display-path files.

<success_criteria>

  • All three all-day TZ sites route through getHouseholdTimezone(db); no duplicated read/fallback.
  • D-05 source-of-truth, D-06 backward-compat, D-07 boundary all satisfied.
  • RED then GREEN commits present. </success_criteria>
Create `.planning/phases/18-auto-timezone-detection-and-ability-to-change-timezone/18-03-SUMMARY.md` when done.