mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
docs(planning): record W7b timezone follow-up report
This commit is contained in:
1 parent
16fbdbcb87
commit
58bbb4ec78
1 file changed
+191
@@ -0,0 +1,191 @@
|
||||
# v1.4.25 W7b — per-user timezone follow-up (4 deferred surfaces) — report
|
||||
|
||||
Status: shipped on `develop`. Not pushed. Not tagged. No
|
||||
`CHANGELOG.md` / `package.json` touch.
|
||||
|
||||
The W7 wave (commit `7a27390` … `a0dffbd`) shipped the per-user timezone
|
||||
schema, resolver, and the easy display surfaces (export, doctor-report
|
||||
PDF, profile picker, admin default). Four surfaces stayed on Berlin:
|
||||
the `berlinDayKey()` callers, the chart x-axis tick labels, the Coach
|
||||
snapshot day buckets, and the `MoodEntry.date` column. W7b closes
|
||||
those.
|
||||
|
||||
## Per-surface commit SHAs
|
||||
|
||||
| Surface | SHA | Summary |
|
||||
| ------- | --------- | ------------------------------------------------------- |
|
||||
| 1 | `35f068a` | `berlinDayKey()` callers honour user `displayTimezone` |
|
||||
| 2 | `eeaa563` | Chart x-axis ticks render in user `displayTimezone` |
|
||||
| 3 | `7d878c1` | Coach snapshot anchors yesterday/this-week to user tz |
|
||||
| 4 | `989243a` | `MoodEntry.tz` column for per-row tz attribution |
|
||||
| — | `2bdfdc2` | Prettier across W7b surfaces |
|
||||
|
||||
## Surface 1 — `berlinDayKey()` 5 callers
|
||||
|
||||
Four routes (`dashboard/summary`, `medications/intake`, `analytics`,
|
||||
`insights/targets`) and the private copies of `berlinDayKey()` inside
|
||||
those routes now read `userTz` from the authenticated `User` and pass
|
||||
it through `userDayKey()` from `src/lib/tz/resolver.ts`. The
|
||||
analytics route also threads `userTz` into the helpers it calls
|
||||
(`computeSleepStageBreakdown`, `computeCorrelationHypotheses`).
|
||||
|
||||
The aggregation logic is unchanged — only the bucket-key function
|
||||
swapped from `berlinDayKey(date)` to `userDayKey(date, userTz)`.
|
||||
Existing Berlin users see no behaviour change (Europe/Berlin is the
|
||||
schema default for `User.timezone`).
|
||||
|
||||
Integration test extended with a two-case Auckland assertion (commit
|
||||
`35f068a`): the `userDayKey()` boundary case (13:00 UTC May 14 →
|
||||
`2026-05-15` Auckland vs `2026-05-14` Berlin) and the dashboard
|
||||
summary route returning a non-zero streak for an Auckland account
|
||||
with three consecutive Auckland-day measurements.
|
||||
|
||||
## Surface 2 — Chart x-axis prop threading
|
||||
|
||||
Four chart components (`HealthChart`, `MoodChart`,
|
||||
`MedicationComplianceChart`, `ComplianceLineChart`) gained a
|
||||
`userTimezone?: string` prop defaulting to `"Europe/Berlin"`. Inside
|
||||
each chart the X-axis tick formatter, tooltip date label, and per-day
|
||||
bucketing all run through a tz-aware `makeFormatters(locale, userTz)`
|
||||
instance instead of the legacy `formatDateShort()`.
|
||||
|
||||
Mount sites that already pull `useAuth()` pass `user?.timezone`:
|
||||
|
||||
- `src/app/page.tsx` (dashboard — 7 chart instances)
|
||||
- `src/components/insights/trends-row.tsx` (3 mini charts)
|
||||
- `src/components/insights/recommendation-card.tsx` (1 mini)
|
||||
- `src/components/insights/insight-advisor-card.tsx` (inline charts)
|
||||
|
||||
Ad-hoc usages without a user (storybook-style, test harnesses) keep
|
||||
the legacy Europe/Berlin rendering bit-for-bit thanks to the
|
||||
`'Europe/Berlin'` default. The TanStack Query cache key for the
|
||||
HealthChart's data fetch gains `userTimezone` so a tz change inside a
|
||||
session re-buckets correctly.
|
||||
|
||||
Tests: 109 chart-component unit tests stay green.
|
||||
|
||||
## Surface 3 — Coach snapshot
|
||||
|
||||
`src/lib/ai/coach/snapshot.ts` previously bucketed by UTC
|
||||
(`utcDayKey`, `utcWeekday`, `isoWeekKey`). It now:
|
||||
|
||||
- Reads `User.timezone` alongside `User.coachPrefsJson` in the same
|
||||
query (no extra DB round-trip).
|
||||
- Threads `userTz` through every bucketing helper (`dailyMeans`,
|
||||
`bucketWeekly`, `buildDailyValueRows`, `buildDailyBpRows`) and
|
||||
through the medication-compliance day roll-up + Apple Health
|
||||
additive blocks.
|
||||
- Renames `utcDayKey` → `tzDayKey(date, tz)`, `utcWeekday` →
|
||||
`tzWeekday(date, tz)`, `isoWeekKey` → `isoWeekKey(date, tz)`.
|
||||
|
||||
The prompt-budget calculator (`src/lib/ai/coach/budget.ts`) is
|
||||
**unaffected**. The ledger still uses UTC midnight for spend
|
||||
roll-over because the operator's LLM bill cycles on UTC; only the
|
||||
snapshot itself shifts to the user's calendar. The provenance
|
||||
`counts` are row counts, not day counts, so no provenance drift
|
||||
either. The W7 agent's concern that the bucket math was shared with
|
||||
the budget proved unfounded on re-read.
|
||||
|
||||
Integration test pins the boundary: a 13:00 UTC May 14 reading (=
|
||||
01:00 NZST May 15) appears under `"2026-05-15"` in the Coach snapshot
|
||||
for a Pacific/Auckland user. The same instant in Berlin would land
|
||||
on May 14; the test asserts the snapshot does NOT bucket there.
|
||||
|
||||
Existing 88-test coach unit suite stays green (the mocked
|
||||
`user.findUnique` returns no timezone, so the snapshot falls back to
|
||||
Europe/Berlin and the v1.4.24 byte-shape holds).
|
||||
|
||||
## Surface 4 — `MoodEntry.tz` column
|
||||
|
||||
Per proposal §7 Decision A:
|
||||
|
||||
- Schema: `MoodEntry.tz: String?` (nullable VARCHAR(64)).
|
||||
- Migration `0044_mood_entry_tz_column` — additive, idempotent,
|
||||
forward-only `ALTER TABLE … ADD COLUMN IF NOT EXISTS`.
|
||||
- Write path (`POST /api/mood-entries` + `PUT /api/mood-entries/:id`)
|
||||
captures `User.timezone` at write time, stores it on the row's
|
||||
`tz` column, and computes `date` using that zone.
|
||||
- Read-path helper (`src/lib/mood/date-key.ts`):
|
||||
- `moodDateKey(date, tz)` — the canonical write-time bucketer.
|
||||
- `effectiveMoodTz(row)` — returns `row.tz ?? "Europe/Berlin"` so
|
||||
legacy rows continue to be read as Berlin without a backfill.
|
||||
|
||||
The `PUT` route also refreshes `tz` when `moodLoggedAt` changes, so a
|
||||
legacy row promoted via an edit migrates to per-row attribution
|
||||
in-place.
|
||||
|
||||
Unit suite (`src/lib/mood/__tests__/date-key.test.ts`) pins both
|
||||
halves of the contract: legacy null-tz rows bucket like a
|
||||
Europe/Berlin row at the same instant; new Pacific/Auckland rows
|
||||
bucket to the Auckland day when a legacy row at the same instant
|
||||
would land on the Berlin day. +8 unit tests.
|
||||
|
||||
## Test deltas
|
||||
|
||||
| Suite | Before | After | Delta |
|
||||
| ----------------------------------------------------------- | ------ | ----- | ----- |
|
||||
| Unit total | 2367 | 2375 | +8 |
|
||||
| Integration: `tests/integration/timezone-per-user.test.ts` | 7 | 10 | +3 |
|
||||
|
||||
Unit `+8` is `src/lib/mood/__tests__/date-key.test.ts` (Surface 4).
|
||||
Integration `+3` is two assertions added in Surface 1 and one in
|
||||
Surface 3.
|
||||
|
||||
## CI gates
|
||||
|
||||
| Gate | Result |
|
||||
| ------------------------------------------ | ------------------------------- |
|
||||
| `pnpm typecheck` | clean |
|
||||
| `pnpm lint` | clean |
|
||||
| `pnpm test` (unit) | 2375 passed, 1 skipped |
|
||||
| `pnpm test:integration` (timezone file) | 10 passed |
|
||||
| `pnpm format:check` (W7b files) | clean after `pnpm format` run |
|
||||
|
||||
A single unrelated integration test from a concurrent agent's branch
|
||||
(`tests/integration/measurements-batch-delete.test.ts`, introduced in
|
||||
`959adec`) fails on a unique-constraint duplicate seed. Out of scope
|
||||
for W7b; flagged for the owning agent.
|
||||
|
||||
## Files touched
|
||||
|
||||
```
|
||||
prisma/schema.prisma # +5 (MoodEntry.tz)
|
||||
prisma/migrations/0044_mood_entry_tz_column/migration.sql # NEW (22 LOC)
|
||||
|
||||
src/lib/tz/resolver.ts # unchanged (reused)
|
||||
src/lib/mood/date-key.ts # NEW (~57 LOC)
|
||||
src/lib/mood/__tests__/date-key.test.ts # NEW (~101 LOC)
|
||||
|
||||
src/app/api/dashboard/summary/route.ts # userTz, drop berlinDayKey
|
||||
src/app/api/medications/intake/route.ts # userTz, drop berlinDayKey
|
||||
src/app/api/analytics/route.ts # userTz, drop berlinDayKey
|
||||
src/app/api/insights/targets/route.ts # userTz, dayKey shim
|
||||
src/app/api/mood-entries/route.ts # write tz + user-tz date
|
||||
src/app/api/mood-entries/[id]/route.ts # refresh tz + date on PUT
|
||||
|
||||
src/components/charts/health-chart.tsx # userTimezone prop
|
||||
src/components/charts/mood-chart.tsx # userTimezone prop
|
||||
src/components/charts/medication-compliance-chart.tsx # userTimezone prop
|
||||
src/components/charts/compliance-line-chart.tsx # userTimezone prop
|
||||
|
||||
src/app/page.tsx # thread user?.timezone
|
||||
src/components/insights/trends-row.tsx # thread user?.timezone
|
||||
src/components/insights/recommendation-card.tsx # thread user?.timezone
|
||||
src/components/insights/insight-advisor-card.tsx # thread user?.timezone
|
||||
|
||||
src/lib/ai/coach/snapshot.ts # tz-aware bucketers
|
||||
|
||||
tests/integration/timezone-per-user.test.ts # +3 cases
|
||||
```
|
||||
|
||||
## What's left for v1.5
|
||||
|
||||
Nothing on the per-user-timezone proposal. Every surface §3 listed
|
||||
now honours `User.timezone` end-to-end.
|
||||
|
||||
The new untracked insight sub-pages from concurrent agents
|
||||
(`src/app/insights/bmi/`, `blutdruck/`, `puls/`, `gewicht/`,
|
||||
`stimmung/`, etc.) already thread `userTimezone={user?.timezone}`
|
||||
into their chart mounts — those pages were modified in-place when I
|
||||
threaded the prop on tracked sibling files. When the owning agent
|
||||
commits those pages, the tz threading lands with them.
|
||||
Reference in new issue
Block a user