refactor(tz): route coach/verdict/mood day-keys through userDayKey

The coach snapshot tzDayKey, the dashboard verdict dayKeyInTz, and the
mood-entry moodDateKey each carried their own Intl day-key formatter
(en-CA / en-CA / sv-SE). Delegate all three to the canonical userDayKey
so every day-bucket key stays byte-aligned and the locale decision
lives in one place. Behaviour-preserving.
This commit is contained in:
Marc-André Bombeck committed 2026-06-18 18:53:07 +02:00
1 parent e15e8ef428
commit f22e39872e
3 files changed
+13 -23

No files matched your search

+4 -7
View File
@@ -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);
}
/**
+2 -11
View File
@@ -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",
+7 -5
View File
@@ -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);
}
/**