fix(tz): one default zone, and the user's own calendar where Berlin was hard-coded

The status assessments folded their recent days, weeks, months and years on
Berlin days whatever the user's zone, the mood and cycle crosstabs keyed
their metric days in Berlin, the resting-pulse proxy defaulted to Berlin
days, the briefing's opener rotated at Berlin midnight, and every
assessment prompt labelled today's date as Berlin's. They now use the
user's zone. Rollup months and years are labelled by their UTC start,
which is how they are cut; read in a zone west of UTC the label named the
month before.

"Europe/Berlin" was written out in some eighty places. DEFAULT_TIMEZONE in
lib/tz/format is now the one definition: the second copy in mood/date-key,
DISPLAY_TIMEZONE, the Berlin-only day key helpers and the per-route copies
are gone. An in-repo lint rule rejects the literal outside lib/tz and
tests; four areas another change owns are listed as pending.
This commit is contained in:
Marc-André Bombeck committed 2026-09-26 16:23:14 +02:00
1 parent c366d4946c
commit 9d2fd1db14
124 files changed
+705 -415

No files matched your search

@@ -69,6 +69,7 @@ import { useViewportWidth } from "@/hooks/use-viewport-width";
import { chooseTickInterval } from "@/lib/charts/x-axis-density";
import { apiGet } from "@/lib/api/api-fetch";
import { shouldFireDataReady } from "@/lib/charts/data-ready-latch";
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
interface DailyCompliancePoint {
/** Berlin calendar day, "YYYY-MM-DD". */
@@ -208,7 +209,7 @@ interface MedicationComplianceChartProps {
export function MedicationComplianceChart({
title,
userTimezone = "Europe/Berlin",
userTimezone = DEFAULT_TIMEZONE,
compareBaseline,
onDataReady,
}: MedicationComplianceChartProps) {
@@ -31,6 +31,7 @@ import {
intakeToastOptions,
runUndoIntake,
} from "@/components/medications/use-medication-intake";
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
/**
* v1.4.37 W7b — dashboard "Hinzufügen" → "Medikamenteneinnahme" quick-add.
@@ -115,7 +116,7 @@ export function MedicationIntakeQuickAdd({
// source the cards use); Berlin stays the last-resort fallback so
// logged-out mounts behave unchanged.
const { user } = useAuth();
const userTz = user?.timezone || "Europe/Berlin";
const userTz = user?.timezone || DEFAULT_TIMEZONE;
// The record this dose lands in, or null in the caller's own.
const recordName = useActiveRecordName();
@@ -19,7 +19,7 @@ import { useAuth } from "@/hooks/use-auth";
import { apiGet } from "@/lib/api/api-fetch";
import { queryKeys } from "@/lib/query-keys";
import { useTranslations, useFormatters } from "@/lib/i18n/context";
import { userDayKey, shiftDateKey } from "@/lib/tz/format";
import { userDayKey, shiftDateKey, DEFAULT_TIMEZONE } from "@/lib/tz/format";
import { TileHeader } from "@/components/insights/tile-header";
import { Button } from "@/components/ui/button";
import { QueryErrorCard } from "@/components/ui/query-error-card";
@@ -94,7 +94,7 @@ export function IntradayPulseChart({
const { isAuthenticated } = useAuth();
const { t } = useTranslations();
const fmt = useFormatters();
const tz = userTimezone ?? "Europe/Berlin";
const tz = userTimezone ?? DEFAULT_TIMEZONE;
const todayKey = userDayKey(new Date(), tz);
// `null` tracks "today" live (so a session left open across midnight keeps
// following the current day); a non-null value pins the viewed day once
@@ -34,7 +34,7 @@ vi.mock("@/hooks/use-measurement-reminders", () => ({
}));
import { VorsorgeSection, postponeOffsetTargets } from "../vorsorge-section";
import { DISPLAY_TIMEZONE } from "@/lib/format-locale";
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
/**
* Noon on the day `offsetDays` from today, counted on the DISPLAY zone's
@@ -53,7 +53,7 @@ import { DISPLAY_TIMEZONE } from "@/lib/format-locale";
function noonOnDisplayDay(offsetDays: number): Date {
// `en-CA` formats as YYYY-MM-DD, which is the shape we want to add days to.
const today = new Intl.DateTimeFormat("en-CA", {
timeZone: DISPLAY_TIMEZONE,
timeZone: DEFAULT_TIMEZONE,
year: "numeric",
month: "2-digit",
day: "2-digit",
@@ -219,7 +219,7 @@ describe("<VorsorgeSection> loading + empty", () => {
// the wall-clock gap to now exceeds the 24h a rolling delta would key on.
// Anchor the due instant to noon on the DISPLAY zone's today, not the
// process clock's. `vitest.config.mts` pins the suite to TZ=UTC while the
// component floors in the profile zone (`DISPLAY_TIMEZONE`, Europe/Berlin),
// component floors in the profile zone (`DEFAULT_TIMEZONE`, Europe/Berlin),
// and those two disagree about the date for the two hours before UTC
// midnight. Anchoring on `setHours` made this case fail every night in
// that window, which is the same confusion of clocks the assertion exists
@@ -1,3 +1,4 @@
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
/**
* v1.12.3 — resolve the canonical slot instant of the dose a medication
* card is currently surfacing, so the card's "Genommen" / "Skip" buttons
@@ -138,7 +139,7 @@ export function resolveDisplayedSlotInstant(
currentWindowStatus,
nextDueAt,
now,
timeZone = "Europe/Berlin",
timeZone = DEFAULT_TIMEZONE,
} = input;
// The card surfaces the current/overdue dose band — record that band's
@@ -111,6 +111,7 @@ import {
type LedgerRow,
} from "@/components/medications/dose-history-ledger-compute";
import { apiDelete, apiGet, apiPost, apiPut } from "@/lib/api/api-fetch";
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
const DAY_MS = 24 * 60 * 60 * 1000;
@@ -182,7 +183,7 @@ export function DoseHistoryLedger({
// marked — the most-encountered of the set.
const recordName = useActiveRecordName();
const queryClient = useQueryClient();
const timeZone = user?.timezone || "Europe/Berlin";
const timeZone = user?.timezone || DEFAULT_TIMEZONE;
const [addOpen, setAddOpen] = useState(false);
const [editingEvent, setEditingEvent] = useState<{
@@ -37,6 +37,7 @@ import {
} from "@/components/medications/detail/supply-runway";
import { useAuth } from "@/hooks/use-auth";
import { useMedicationComplianceSummary } from "@/lib/queries/use-medication-compliance-summary";
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
/**
* v1.4.25 W4d — GLP-1 medication card variant.
@@ -202,7 +203,7 @@ export function Glp1MedicationCard({
// v1.16.9 — the card reasons in the PROFILE timezone; Berlin stays the
// last-resort fallback for logged-out mounts and legacy fixtures.
const { user } = useAuth();
const userTz = user?.timezone || "Europe/Berlin";
const userTz = user?.timezone || DEFAULT_TIMEZONE;
const fmt = useFormatters();
// Issue #490 — the zone `fmt` renders dates in (mirror → Berlin). The
// weekday name paired with `fmt.dateShort` must be derived in this SAME
@@ -44,6 +44,7 @@ import {
runLogIntake,
runUndoIntake,
} from "@/components/medications/use-medication-intake";
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
interface LogIntakeSchedule extends ScheduleWindowInput {
label: string | null;
@@ -120,7 +121,7 @@ export function LogIntakeDialog({
const recordName = useActiveRecordName();
const queryClient = useQueryClient();
const { user } = useAuth();
const userTz = user?.timezone || "Europe/Berlin";
const userTz = user?.timezone || DEFAULT_TIMEZONE;
const formId = useId();
const [medicationOverride, setMedicationOverride] = useState<string | null>(
@@ -34,6 +34,7 @@ import { LogInjectionSiteDialog } from "@/components/medications/log-injection-s
import { useGlobalExcludedInjectionSites } from "@/lib/medications/use-injection-site-prefs";
import { useAuth } from "@/hooks/use-auth";
import type { InjectionSiteKey } from "@/lib/medications/injection-sites";
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
interface Schedule {
id: string;
@@ -149,7 +150,7 @@ export function MedicationCard({
// session carries); Berlin stays the last-resort fallback so logged-out
// mounts and legacy fixtures behave unchanged.
const { user } = useAuth();
const userTz = user?.timezone || "Europe/Berlin";
const userTz = user?.timezone || DEFAULT_TIMEZONE;
const fmt = useFormatters();
const weekdayLabel = useWeekdayLabel();
// v1.8.5 — post-dose injection-site prompt state. Holds the intake
@@ -43,6 +43,7 @@ import {
} from "@/lib/queries/use-medication-compliance-summary";
import { useAuth } from "@/hooks/use-auth";
import { cn } from "@/lib/utils";
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
/**
* v1.16.10 — the compact table view for /medications (issue #316 item 3).
@@ -194,7 +195,7 @@ export function MedicationTable({
}: MedicationTableProps) {
const { t, locale } = useTranslations();
const { user } = useAuth();
const userTz = user?.timezone || "Europe/Berlin";
const userTz = user?.timezone || DEFAULT_TIMEZONE;
const [sort, setSort] = useState<MedicationTableSort | null>(initialSort);
const [, forceUpdate] = useReducer((x: number) => x + 1, 0);
+2 -1
View File
@@ -57,6 +57,7 @@ import {
} from "@/lib/medications/window-status";
import { resolveDisplayedSlotInstant } from "@/components/medications/card-parts/displayed-slot-instant";
import type { DoseStatus } from "@/lib/analytics/compliance";
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
type Translator = (
key: string,
@@ -142,7 +143,7 @@ export function deriveDueMedications(
} = {},
): DueMedication[] {
const now = options.now ?? new Date();
const tz = options.tz ?? "Europe/Berlin";
const tz = options.tz ?? DEFAULT_TIMEZONE;
const { lateMinutes, missedMinutes } =
options.thresholds ?? DEFAULT_THRESHOLDS;
@@ -33,7 +33,7 @@ import { TimeFormatSelect } from "@/components/settings/time-format-select";
import { DateFormatSelect } from "@/components/settings/date-format-select";
import { UnitPreferenceSelect } from "@/components/settings/unit-preference-select";
import { GlucoseUnitSelect } from "@/components/settings/glucose-unit-select";
import { detectBrowserTimezone } from "@/lib/tz/format";
import { detectBrowserTimezone, DEFAULT_TIMEZONE } from "@/lib/tz/format";
import { apiFetchRaw } from "@/lib/api/api-fetch";
import {
describeRejectedProfileField,
@@ -74,7 +74,7 @@ export function AccountSection() {
const [height, setHeight] = useState<HeightDraft>(EMPTY_HEIGHT_DRAFT);
const [dateOfBirth, setDateOfBirth] = useState("");
const [gender, setGender] = useState<string>("");
const [timezone, setTimezone] = useState<string>("Europe/Berlin");
const [timezone, setTimezone] = useState<string>(DEFAULT_TIMEZONE);
// v1.7.0 — optional patient-identity fields for the health-record export.
const [fullName, setFullName] = useState("");
const [insurerName, setInsurerName] = useState("");
@@ -84,7 +84,7 @@ export function AccountSection() {
height: EMPTY_HEIGHT_DRAFT,
dateOfBirth: "",
gender: "",
timezone: "Europe/Berlin",
timezone: DEFAULT_TIMEZONE,
fullName: "",
insurerName: "",
insuranceNumber: "",
@@ -39,6 +39,7 @@ import { Switch } from "@/components/ui/switch";
import { SettingsCardActions } from "./_card-actions";
import { SettingsCardHeader } from "./_card-header";
import { SettingsCard } from "./settings-card";
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
interface ManagedRecordSettingsResponse {
recordId: string;
@@ -414,7 +415,7 @@ function ProfileSettingsForm({
<label className="grid gap-1 text-sm" htmlFor="managed-timezone">
{t("settings.sharedRecord.managedSettings.profile.timezone")}
<Input
defaultValue={asString(settings.timezone, "Europe/Berlin")}
defaultValue={asString(settings.timezone, DEFAULT_TIMEZONE)}
disabled={disabled}
id="managed-timezone"
name="timezone"
+2 -2
View File
@@ -26,7 +26,7 @@ import { useMemo } from "react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { NativeSelect } from "@/components/ui/native-select";
import { listSupportedTimezones } from "@/lib/tz/format";
import { listSupportedTimezones, DEFAULT_TIMEZONE } from "@/lib/tz/format";
import { useTranslations } from "@/lib/i18n/context";
// v1.4.27 MB7 / CF-52 — the in-file `NATIVE_SELECT_CLASS` constant
@@ -67,7 +67,7 @@ export function TimezonePicker({
id={id}
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder="Europe/Berlin"
placeholder={DEFAULT_TIMEZONE}
autoComplete="off"
/>
<p className="text-muted-foreground text-xs">{labelHint}</p>