diff --git a/src/lib/ai/coach/snapshot.ts b/src/lib/ai/coach/snapshot.ts index c7639ecfb..f8e82811a 100644 --- a/src/lib/ai/coach/snapshot.ts +++ b/src/lib/ai/coach/snapshot.ts @@ -23,6 +23,7 @@ import { type CoachDataCluster, } from "@/lib/validations/coach-prefs"; import { DEFAULT_TIMEZONE } from "@/lib/tz/resolver"; +import { userDayKey } from "@/lib/tz/format"; import { convertGlucose, resolveGlucoseUnit } from "@/lib/glucose"; import { computeGlucoseClinicalMetrics, @@ -223,15 +224,11 @@ function windowToDays(window: CoachScopeWindow): number { * "last night" answer has to use the user's clock, not UTC. Up to * v1.4.24 the day-key was UTC, so a 23:50 Pacific/Auckland reading * landed in the next UTC day's bucket and the Coach couldn't pair it - * with the user's mental model. + * with the user's mental model. Delegates to the canonical + * `userDayKey` so every day-bucket surface stays byte-aligned. */ function tzDayKey(date: Date, tz: string): string { - return new Intl.DateTimeFormat("en-CA", { - timeZone: tz, - year: "numeric", - month: "2-digit", - day: "2-digit", - }).format(date); + return userDayKey(date, tz); } /** diff --git a/src/lib/dashboard/verdict.ts b/src/lib/dashboard/verdict.ts index 59c62e330..101667268 100644 --- a/src/lib/dashboard/verdict.ts +++ b/src/lib/dashboard/verdict.ts @@ -31,6 +31,7 @@ import { COACH_NUDGE_SLEEP_DEFICIT_MARGIN_H, } from "@/lib/jobs/coach-nudge-thresholds"; import { buildWeightRangeFromHeight } from "@/lib/analytics/value-bands"; +import { userDayKey } from "@/lib/tz/format"; import type { DashboardSnapshot } from "@/lib/dashboard/snapshot"; const DAY_MS = 24 * 60 * 60 * 1000; @@ -93,16 +94,6 @@ export interface DashboardVerdict { cta: DashboardVerdictCta | null; } -/** YYYY-MM-DD key of `d`'s calendar day in `tz`. */ -function dayKeyInTz(d: Date, tz: string): string { - return new Intl.DateTimeFormat("en-CA", { - timeZone: tz, - year: "numeric", - month: "2-digit", - day: "2-digit", - }).format(d); -} - /** HH:mm wall-clock of `d` in `tz`. */ function hmInTz(d: Date, tz: string): string { const parts = new Intl.DateTimeFormat("en-GB", { @@ -176,7 +167,7 @@ export function resolveDashboardVerdict( if ( untilDue >= 0 && untilDue <= DOSE_UPCOMING_WINDOW_MS && - dayKeyInTz(new Date(dueMs), tz) === dayKeyInTz(now, tz) + userDayKey(new Date(dueMs), tz) === userDayKey(now, tz) ) { return { variant: "doseUpcoming", diff --git a/src/lib/mood/date-key.ts b/src/lib/mood/date-key.ts index eb7801697..440561f0a 100644 --- a/src/lib/mood/date-key.ts +++ b/src/lib/mood/date-key.ts @@ -28,6 +28,8 @@ * legacy date string (`effectiveMoodTz`). */ +import { userDayKey } from "@/lib/tz/format"; + export const DEFAULT_TIMEZONE = "Europe/Berlin"; /** @@ -35,14 +37,14 @@ export const DEFAULT_TIMEZONE = "Europe/Berlin"; * timezone at write time. Used by `POST /api/mood-entries` and the * `PUT /api/mood-entries/:id` route when `moodLoggedAt` changes. * - * The Swedish locale ("sv-SE") is the historical choice for ISO 8601 - * day output via `Intl.DateTimeFormat`; we keep it so the new path - * produces byte-identical strings to the legacy `toBerlinDate()` for - * Europe/Berlin users (the only path tested under the v1.4.24 suite). + * Delegates to the canonical `userDayKey` so the mood day-bucket key + * stays byte-identical to every other day-bucket surface. The ISO + * YYYY-MM-DD shape is unchanged from the legacy sv-SE formatter; the + * empty-tz fallback to Europe/Berlin is preserved here. */ export function moodDateKey(date: Date, tz: string): string { const safe = tz && tz.length > 0 ? tz : DEFAULT_TIMEZONE; - return new Intl.DateTimeFormat("sv-SE", { timeZone: safe }).format(date); + return userDayKey(date, safe); } /**