From 9798d795a7b0b8b22730df8d51c9e0f9bf76de9e Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Sun, 14 Jun 2026 22:33:39 -0400 Subject: [PATCH] docs(18-03): complete broker rewire plan --- .planning/ROADMAP.md | 4 +- .planning/STATE.md | 14 +- .../18-03-SUMMARY.md | 136 ++++++++++++++++++ 3 files changed, 146 insertions(+), 8 deletions(-) create mode 100644 .planning/phases/18-auto-timezone-detection-and-ability-to-change-timezone/18-03-SUMMARY.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 0c5a608..4fb60f5 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:** 2/4 plans executed +**Plans:** 3/4 plans executed Plans: **Wave 1** @@ -646,7 +646,7 @@ Plans: **Wave 2** *(blocked on Wave 1 completion)* - [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) +- [x] 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 59cf015..43b9d3b 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -3,14 +3,14 @@ gsd_state_version: 1.0 milestone: v1.1 milestone_name: Operability & Polish status: executing -stopped_at: Phase 18 context gathered -last_updated: "2026-06-15T02:13:43.020Z" +stopped_at: Phase 18 Plan 03 complete — broker rewire done; plan 4 of 4 is next +last_updated: "2026-06-15T02:33:25.687Z" last_activity: 2026-06-15 progress: total_phases: 23 completed_phases: 9 total_plans: 36 - completed_plans: 34 + completed_plans: 35 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: 3 of 4 +Plan: 4 of 4 Status: Ready to execute Last activity: 2026-06-15 @@ -109,6 +109,7 @@ _Updated after each plan completion_ | 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 | +| Phase 18 P03 | 28 | 2 tasks | 4 files | ## Accumulated Context @@ -186,6 +187,7 @@ Recent decisions affecting current work: - [Phase ?]: D-CLIENT-TYPES: reminderLeadMinutes required on CalendarOccurrence, optional on CreateEventPayload (absent=no-change D-08) - [Phase ?]: D-PAYLOAD-ABSENT: __custom__ unchanged → field omitted from payload; server hasOwnProperty check preserves original VALARM (D-08) - [Phase ?]: D-NULL-FALLBACK: occurrence.reminderLeadMinutes===null mapped to None; occurrence cannot distinguish absolute/multi-VALARM from no-reminder; rely on server-side preserve (absent payload) +- [Phase ?]: D-05/18-03: three all-day broker sites now route through getHouseholdTimezone(db) ### Roadmap Evolution @@ -254,8 +256,8 @@ Recent decisions affecting current work: ## Session Continuity -Last session: 2026-06-15T02:13:43.000Z -Stopped at: Phase 18 context gathered +Last session: 2026-06-15T02:33:25.669Z +Stopped at: Phase 18 Plan 03 complete — broker rewire done; plan 4 of 4 is next Resume file: None ## Operator Next Steps diff --git a/.planning/phases/18-auto-timezone-detection-and-ability-to-change-timezone/18-03-SUMMARY.md b/.planning/phases/18-auto-timezone-detection-and-ability-to-change-timezone/18-03-SUMMARY.md new file mode 100644 index 0000000..9406ee9 --- /dev/null +++ b/.planning/phases/18-auto-timezone-detection-and-ability-to-change-timezone/18-03-SUMMARY.md @@ -0,0 +1,136 @@ +--- +phase: 18-auto-timezone-detection-and-ability-to-change-timezone +plan: "03" +subsystem: api +tags: [timezone, broker, reminder-scheduler, outbox-worker, tdd, drizzle] + +# Dependency graph +requires: + - phase: 18-auto-timezone-detection-and-ability-to-change-timezone + plan: "01" + provides: getHouseholdTimezone(db) — the accessor imported at all three rewired sites +provides: + - reminderScheduler.ts all-day branch reads stored household_timezone via getHouseholdTimezone(db) + - outboxWorker.ts UPDATE all-day branch reads stored household_timezone via getHouseholdTimezone(db) + - outboxWorker.ts CREATE all-day branch reads stored household_timezone via getHouseholdTimezone(db) + +affects: + - 18-04-PLAN (PWA settings UI — all three rewired sites now respect the timezone set via Plan 18-02 admin API) + +# Tech tracking +tech-stack: + added: [] + patterns: + - getHouseholdTimezone(db) imported and awaited at three all-day broker sites (replacing bare process.env.TZ ?? Intl) + - Mock chain extension: makeAppConfigSelectMock for where().limit() chain (different from makeSelectMock which handles innerJoin chains) + - mockTwoQueries updated to mock the new third db.select() call, keeping existing tests green via D-06 fallback + - wireMockChain() extended in outboxWorker.test.ts to handle app_config table with where().limit() returning [] + +key-files: + created: [] + modified: + - 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 + +key-decisions: + - "D-05 wired: three all-day sites replaced bare process.env.TZ ?? Intl with await getHouseholdTimezone(db)" + - "D-06 preserved: getHouseholdTimezone falls back to process.env.TZ → Intl when no app_config row exists; existing tests pin process.env.TZ and pass unchanged" + - "D-07 enforced: eventDateTime.ts and hydrateEvents.ts not modified (verified via git diff)" + - "mockTwoQueries extended to emit a third mockReturnValueOnce for the new app_config SELECT (no-row → D-06 fallback)" + - "mockReset() added in mockTwoQueries/mockThreeQueries to clear unconsumed once-queue entries across vi.resetModules() cycles (Vitest caches mock factory instances)" + +requirements-completed: [D-05, D-06, D-07] + +# Metrics +duration: 28min +completed: 2026-06-15 +--- + +# Phase 18 Plan 03: Broker Rewire — All-Day TZ Sites Summary + +**Three all-day "9 AM local" reminder call sites in reminderScheduler.ts and outboxWorker.ts now route through `getHouseholdTimezone(db)` instead of the bare `process.env.TZ ?? Intl` expression, making the stored household_timezone the source of truth for all-day reminder fire times (D-05/D-06/D-07)** + +## Performance + +- **Duration:** 28 min +- **Started:** 2026-06-15T02:04:00Z +- **Completed:** 2026-06-15T02:32:01Z +- **Tasks:** 2 (TDD RED + GREEN) +- **Files modified:** 4 + +## Accomplishments + +- Rewired `reminderScheduler.ts` line 247: `const serverTz = await getHouseholdTimezone(db);` +- Rewired `outboxWorker.ts` UPDATE branch (~line 501): `const tz = await getHouseholdTimezone(db);` +- Rewired `outboxWorker.ts` CREATE branch (~line 607): `const tz = await getHouseholdTimezone(db);` +- Added 4 new RED tests (2 scheduler + 2 outbox) verifying stored 'America/Chicago' drives the 9 AM alert instant; all correctly FAIL before the rewire +- Extended test mock infrastructure: `makeAppConfigSelectMock`, `mockThreeQueries`, updated `mockTwoQueries` to add third call for D-06 fallback path, updated `wireMockChain()` in outboxWorker.test.ts for app_config table +- All 76 broker tests pass after GREEN commit; TypeScript typecheck (`tsc --noEmit`) clean +- D-07 boundary confirmed: `eventDateTime.ts` and `hydrateEvents.ts` not in changed-files list + +## Task Commits + +1. **Task 1: RED — failing stored-TZ all-day tests** - `94daca3` (test) +2. **Task 2: GREEN — rewire all three all-day TZ call sites** - `c80845c` (feat) + +## Files Created/Modified + +- `apps/api/src/broker/reminderScheduler.ts` — import added + line 247 rewired to `await getHouseholdTimezone(db)` +- `apps/api/src/broker/outboxWorker.ts` — import added + lines ~501 and ~607 rewired to `await getHouseholdTimezone(db)` +- `apps/api/tests/broker/reminderScheduler.test.ts` — `makeAppConfigSelectMock`, `mockThreeQueries` helpers added; `mockTwoQueries` extended to mock the new third db.select(); Plan 18-03 describe block with 2 stored-TZ tests +- `apps/api/tests/broker/outboxWorker.test.ts` — `wireMockChain()` extended for app_config table; `wireMockChainWithTz()` helper for D-05 stored-TZ tests; Plan 18-03 describe block with 2 stored-TZ tests (create + update branch) + +## Decisions Made + +- `mockTwoQueries` was extended (not renamed) to avoid updating 20+ call sites. The third mock call returns an empty app_config row (no stored TZ → D-06 fallback), which is transparent to all existing tests that pin `process.env.TZ`. +- `mockReset()` added inside `mockTwoQueries` and `mockThreeQueries` to flush any unconsumed `mockReturnValueOnce` entries. Vitest caches mock factory instances across `vi.resetModules()` cycles, so the pending third entry from one test bleeds into the next test's queue without an explicit reset. +- The outbox UPDATE branch test uses `mockWhereCalEvents.mockResolvedValue([{etag: 'W/"abc"'}])` after `wireMockChainWithTz` to drive the freshEtagRows path into the all-day VALARM branch (no rawVevent → falls through to the explicit-reminder/allDay condition). + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 1 - Bug] vi.fn() queue contamination across tests with vi.resetModules()** +- **Found during:** Task 1 (RED) — second scheduler test crashed with "innerJoin is not a function" +- **Issue:** `vi.clearAllMocks()` does not flush `mockReturnValueOnce` queues. Vitest's mock factory for `vi.mock()` returns the SAME `vi.fn()` instance across module resets, so an unconsumed third entry from the first test bled into the first call slot of the second test — causing `makeAppConfigSelectMock` to be returned where `makeSelectMock` was expected. +- **Fix:** Added `db.select.mockReset()` at the start of both `mockTwoQueries` and `mockThreeQueries` to explicitly clear the queue before configuring new once-values. +- **Files modified:** `apps/api/tests/broker/reminderScheduler.test.ts` + +**2. [Rule 2 - Missing critical functionality] wireMockChain() in outboxWorker.test.ts didn't handle app_config** +- **Found during:** Task 2 (GREEN) — the existing CAL-13 all-day test would have broken after the rewire because `app_config` fell through to `{ where: mockWherePending }`, and `mockWherePending(...)` returns a Promise; calling `.limit(1)` on a Promise throws "limit is not a function" +- **Fix:** Extended `wireMockChain()` to handle `app_config` table with a `{ where: fn → {limit: fn} }` chain returning empty rows (D-06 fallback), matching the `getHouseholdTimezone` SELECT chain +- **Files modified:** `apps/api/tests/broker/outboxWorker.test.ts` + +**3. [Rule 2 - Missing critical functionality] mockTwoQueries needed a third mock for the new db.select() call** +- **Found during:** Task 2 (GREEN) — all 20 existing scheduler tests that call `mockTwoQueries` started failing because `runReminderCheck` now issues a third `db.select()` for `getHouseholdTimezone`; the undefined return caused crashes +- **Fix:** Updated `mockTwoQueries` to add a third `mockReturnValueOnce(makeAppConfigSelectMock(null))` returning no row, keeping the D-06 fallback path for all existing tests +- **Files modified:** `apps/api/tests/broker/reminderScheduler.test.ts` + +## Known Stubs + +None — all three rewired sites now read from the actual DB through `getHouseholdTimezone(db)`. No placeholder values. + +## Threat Flags + +None — no new network endpoints, auth paths, file access patterns, or schema changes. The three rewired sites are read-only DB lookups within the trusted server process (T-18-08 disposition: mitigate → validated values only reach computeAlertInstantUtc via the Plan 01 accessor that enforces the D-06 fallback chain). + +## TDD Gate Compliance + +- RED gate: `94daca3` — `test(18-03): add failing stored-TZ all-day tests for scheduler + outbox` +- GREEN gate: `c80845c` — `feat(18-03): route all-day reminder TZ through stored household_timezone` +- REFACTOR gate: N/A (implementation was clean on first pass; deviations were auto-fixed inline) + +## Self-Check: PASSED + +- `apps/api/src/broker/reminderScheduler.ts` — FOUND +- `apps/api/src/broker/outboxWorker.ts` — FOUND +- `apps/api/tests/broker/reminderScheduler.test.ts` — FOUND +- `apps/api/tests/broker/outboxWorker.test.ts` — FOUND +- Commit `94daca3` — FOUND +- Commit `c80845c` — FOUND + +--- +*Phase: 18-auto-timezone-detection-and-ability-to-change-timezone* +*Completed: 2026-06-15*