diff --git a/src/lib/measurements/rollups.ts b/src/lib/measurements/rollups.ts index 41c23336e..897789a30 100644 --- a/src/lib/measurements/rollups.ts +++ b/src/lib/measurements/rollups.ts @@ -31,6 +31,7 @@ import { prisma } from "@/lib/db"; import { getGlobalBoss } from "@/lib/jobs/boss-instance"; import { annotate } from "@/lib/logging/context"; +import { startOfUtcDay } from "@/lib/tz/start-of-utc-day"; import type { MeasurementType, RollupGranularity, @@ -518,17 +519,6 @@ async function persistRollupRows( return touched; } -/** - * Truncate a `Date` to the start of its UTC day. The rollup table - * stores TIMESTAMPTZ values so we anchor on UTC to keep the bucket - * key deterministic across timezone-shifting consumers. - */ -function startOfUtcDay(d: Date): Date { - return new Date( - Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), 0, 0, 0, 0), - ); -} - /** * Span of the bucket containing `measuredAt` at the given granularity. * Postgres `date_trunc` is the canonical truncator; we mirror its diff --git a/src/lib/mood/rollups.ts b/src/lib/mood/rollups.ts index 4601db8e8..bb7df0069 100644 --- a/src/lib/mood/rollups.ts +++ b/src/lib/mood/rollups.ts @@ -40,6 +40,7 @@ import { prisma } from "@/lib/db"; import { getGlobalBoss } from "@/lib/jobs/boss-instance"; import { annotate } from "@/lib/logging/context"; +import { startOfUtcDay } from "@/lib/tz/start-of-utc-day"; import type { Prisma, PrismaClient, @@ -611,18 +612,6 @@ async function persistMoodRollupRows( return touched; } -/** - * Truncate a `Date` to the start of its UTC day. The rollup table - * stores TIMESTAMPTZ values so we anchor on UTC to keep the bucket - * key deterministic across timezone-shifting consumers — same - * convention as `measurement-rollups`. - */ -function startOfUtcDay(d: Date): Date { - return new Date( - Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), 0, 0, 0, 0), - ); -} - /** * Span of the bucket containing `moodLoggedAt` at the given * granularity. Mirrors `measurement-rollups` so the WEEK / MONTH / diff --git a/src/lib/tz/start-of-utc-day.ts b/src/lib/tz/start-of-utc-day.ts new file mode 100644 index 000000000..eb9cc53be --- /dev/null +++ b/src/lib/tz/start-of-utc-day.ts @@ -0,0 +1,20 @@ +/** + * UTC-anchored start-of-day truncation shared by the measurement + + * mood rollup writers. + * + * Both rollup tables store TIMESTAMPTZ bucket starts. Anchoring on the + * UTC calendar day (rather than the user's local day) keeps the bucket + * key deterministic across timezone-shifting consumers — the same + * convention `date_trunc('day', measured_at AT TIME ZONE 'UTC')` would + * produce on the read side. The display surfaces re-bucket into the + * user's timezone via `userDayKey()` from `./format`. + * + * v1.4.40 W-GHOSTS consolidated two byte-identical local copies + * (`src/lib/measurements/rollups.ts` + `src/lib/mood/rollups.ts`) into + * this module so future writers cannot drift independently. + */ +export function startOfUtcDay(d: Date): Date { + return new Date( + Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), 0, 0, 0, 0), + ); +}