fix(medications): bucket the local day in the user's zone, DST-safe

Five timezone-correctness fixes across the medication / compliance surfaces:

- getUserTodayBounds now derives both bounds through the DST-safe
  startOfLocalDayInTz, so the today window is 23h on spring-forward and
  25h on fall-back instead of a hardcoded 24h sampled at now. A 23:30
  local dose no longer drops off the med cards, dashboard, PWA badge and
  reminder idempotency near midnight on a transition day.
- dailyComplianceRatesFromLedger takes the user's zone and buckets each
  per-day rate with userDayKey, so a non-Berlin user's evening dose is
  filed on the local day it was due rather than the next Berlin day.
- classifyIntakeTiming interprets the window HH:mm in an optional zone;
  the achievements perfect-day check passes the user's zone and reads the
  scheduled minute-of-day in-zone, so a dose far from UTC is graded
  against local wall time.
- The /insights greeting resolves the hour in the user's stored zone via
  the new hourInTz helper, matching the dashboard hero.
- The doctor-report and schedule-anchored compliance queries load
  schedules through SCHEDULE_COMPLIANCE_SELECT instead of a broad
  include, so a future engine column reaches both surfaces.
This commit is contained in:
Marc-André Bombeck committed 2026-07-08 23:38:48 +02:00
1 parent 6ad2dc858a
commit fb6813bcdb
10 files changed
+99 -51

No files matched your search

+10 -3
View File
@@ -7,6 +7,7 @@ import { useTranslations, useFormatters } from "@/lib/i18n/context";
import { formatUpdatedLabel } from "@/lib/i18n/relative-time";
import { useAuth } from "@/hooks/use-auth";
import { useModuleEnabled } from "@/hooks/use-module-enabled";
import { hourInTz, DEFAULT_TIMEZONE } from "@/lib/tz/format";
import { ProseBlocks } from "@/components/insights/prose-blocks";
import { cn } from "@/lib/utils";
import {
@@ -138,8 +139,7 @@ interface HeroStripProps {
* rare to see /insights at 03:00 and "Guten
* Morgen" pre-5am reads odd)
*/
function resolveGreetingKey(now: Date): string {
const hour = now.getHours();
function resolveGreetingKey(hour: number): string {
if (hour >= 5 && hour < 12) return "insights.heroGreetingMorning";
if (hour >= 12 && hour < 18) return "insights.heroGreetingAfternoon";
// 18:00-22:59 maps to "Good evening"; 23:00-04:59 also maps to
@@ -221,7 +221,14 @@ export function HeroStrip({
// pillar from the number itself). SSR-safe + default-on, matching the
// disabled-allowlist gate.
const moodEnabled = useModuleEnabled("mood");
const greetingKey = resolveGreetingKey(now ?? new Date());
// Greeting hour in the user's configured zone, not the browser's — a
// traveller / VPN user / wrong device clock otherwise sees the wrong
// salutation. Mirrors the dashboard hero (`hourInTimezone(now, userTz)`).
const greetingHour = hourInTz(
now ?? new Date(),
user?.timezone ?? DEFAULT_TIMEZONE,
);
const greetingKey = resolveGreetingKey(greetingHour);
const greetingBase = t(greetingKey);
const greeting = userName ? `${greetingBase}, ${userName}` : greetingBase;
const subtitle = briefing?.paragraph ?? t("insights.heroFallbackSubtitle");