diff --git a/eslint-plugins/healthlog/__tests__/no-default-zone-literal.test.mjs b/eslint-plugins/healthlog/__tests__/no-default-zone-literal.test.mjs
new file mode 100644
index 000000000..8213cc9b4
--- /dev/null
+++ b/eslint-plugins/healthlog/__tests__/no-default-zone-literal.test.mjs
@@ -0,0 +1,75 @@
+/**
+ * Unit tests for the `healthlog/no-default-zone-literal` rule: the literal is
+ * banned in application source, allowed in `src/lib/tz/` and in tests, and
+ * the pending files are exempt until their owner switches them.
+ */
+import { describe, it } from "vitest";
+import { RuleTester } from "eslint";
+import rule from "../no-default-zone-literal.js";
+
+RuleTester.describe = describe;
+RuleTester.it = it;
+RuleTester.itOnly = it.only;
+
+const ruleTester = new RuleTester({
+ languageOptions: {
+ ecmaVersion: 2023,
+ sourceType: "module",
+ parserOptions: { ecmaFeatures: { jsx: true } },
+ },
+});
+
+ruleTester.run("no-default-zone-literal", rule, {
+ valid: [
+ {
+ code: 'import { DEFAULT_TIMEZONE } from "@/lib/tz/format"; const tz = user.timezone || DEFAULT_TIMEZONE;',
+ filename: "/repo/src/app/api/insights/cards/route.ts",
+ },
+ {
+ // The one definition.
+ code: 'export const DEFAULT_TIMEZONE = "Europe/Berlin";',
+ filename: "/repo/src/lib/tz/format.ts",
+ },
+ {
+ // Fixtures pick a concrete zone on purpose.
+ code: 'const TZ = "Europe/Berlin";',
+ filename: "/repo/src/lib/insights/__tests__/graded-series.test.ts",
+ },
+ {
+ // Comments are not checked.
+ code: "// read on Europe/Berlin days before v1.39.3\nconst x = 1;",
+ filename: "/repo/src/lib/insights/graded-series.ts",
+ },
+ {
+ // Pending: owned by another change, switched at merge.
+ code: 'const DEFAULT_TZ = "Europe/Berlin";',
+ filename: "/repo/src/lib/medications/window-status.ts",
+ },
+ {
+ code: 'const other = "America/New_York";',
+ filename: "/repo/src/lib/insights/graded-series.ts",
+ },
+ ],
+ invalid: [
+ {
+ code: 'const tz = user.timezone || "Europe/Berlin";',
+ filename: "/repo/src/app/api/insights/cards/route.ts",
+ errors: [{ messageId: "zoneLiteral" }],
+ },
+ {
+ code: 'readDayAggregates({ timeZone: "Europe/Berlin" });',
+ filename: "/repo/src/lib/insights/graded-series.ts",
+ errors: [{ messageId: "zoneLiteral" }],
+ },
+ {
+ code: "const label = `Date: ${todayKey} (Europe/Berlin)`;",
+ filename: "/repo/src/lib/ai/prompts/pulse.ts",
+ errors: [{ messageId: "zoneLiteral" }],
+ },
+ {
+ code: '',
+ filename: "/repo/src/components/settings/timezone-picker.tsx",
+ errors: [{ messageId: "zoneLiteral" }],
+ },
+ ],
+});
diff --git a/eslint-plugins/healthlog/index.js b/eslint-plugins/healthlog/index.js
index 8147b1540..961372c8b 100644
--- a/eslint-plugins/healthlog/index.js
+++ b/eslint-plugins/healthlog/index.js
@@ -10,6 +10,8 @@
* and no off-scale `5` step on a bg-card shell.
* - `job-handler-outcome` — pg-boss bindings route through createAndWork,
* whose handler returns a JobOutcome.
+ * - `no-default-zone-literal` — the default zone is DEFAULT_TIMEZONE from
+ * src/lib/tz/format, never written out.
*/
"use strict";
@@ -20,6 +22,7 @@ const apiFetchRequired = require("./api-fetch-required.js");
const noRawPaletteColor = require("./no-raw-palette-color.js");
const spacingScale = require("./spacing-scale.js");
const jobHandlerOutcome = require("./job-handler-outcome.js");
+const noDefaultZoneLiteral = require("./no-default-zone-literal.js");
module.exports = {
rules: {
@@ -34,5 +37,6 @@ module.exports = {
// the semantic sweep). See the rule header.
"no-dracula-utility": noRawPaletteColor,
"job-handler-outcome": jobHandlerOutcome,
+ "no-default-zone-literal": noDefaultZoneLiteral,
},
};
diff --git a/eslint-plugins/healthlog/no-default-zone-literal.js b/eslint-plugins/healthlog/no-default-zone-literal.js
new file mode 100644
index 000000000..f3a26edc3
--- /dev/null
+++ b/eslint-plugins/healthlog/no-default-zone-literal.js
@@ -0,0 +1,102 @@
+/**
+ * @fileoverview ESLint rule — the default zone is named in one place.
+ *
+ * `"Europe/Berlin"` was written out in some eighty places: fallbacks for a
+ * user with no zone, day keys cut in Berlin whatever the user's zone, a
+ * graded series folded on Berlin days, a prompt that labelled every date as
+ * Berlin's. Each copy was a place where a user outside that zone got
+ * someone else's calendar. `DEFAULT_TIMEZONE` in `src/lib/tz/format.ts` is
+ * the one definition; everything else imports it, and a surface that means
+ * the user's own zone asks for it (`resolveUserTimezone`).
+ *
+ * The rule flags a string literal or template element containing
+ * `Europe/Berlin` in `src/`, outside `src/lib/tz/` and outside test files
+ * (fixtures pick concrete zones on purpose). Comments are not checked.
+ *
+ * WHAT THIS RULE DOES NOT CATCH
+ *
+ * A zone assembled from parts (`"Europe/" + city`), a different hard-coded
+ * zone, or a Berlin day computed through an `Intl.DateTimeFormat` built from
+ * a variable. It removes the literal, not every way to hard-code a calendar.
+ *
+ * `PENDING` lists files another change owns at the moment; they switch to the
+ * constant when that change lands, and the list shrinks to nothing.
+ */
+
+"use strict";
+
+const ZONE = "Europe/Berlin";
+
+const EXEMPT_ROOTS = ["src/lib/tz/"];
+
+// Owned by the medication-scheduling change in flight; switched at merge.
+const PENDING = [
+ "src/lib/medications/",
+ "src/lib/jobs/measurement-reminder.ts",
+ "src/lib/measurement-reminders/",
+ "src/app/api/measurement-reminders/",
+];
+
+function toPosix(filename) {
+ return filename.replace(/\\/g, "/");
+}
+
+function isTestFile(posix) {
+ return (
+ /\.test\.[cm]?[jt]sx?$/.test(posix) ||
+ /\.spec\.[cm]?[jt]sx?$/.test(posix) ||
+ posix.includes("/__tests__/") ||
+ posix.includes("/__mocks__/")
+ );
+}
+
+function isEnforced(filename) {
+ const posix = toPosix(filename);
+ if (!posix.includes("/src/") && !posix.startsWith("src/")) return false;
+ if (EXEMPT_ROOTS.some((root) => posix.includes(root))) return false;
+ if (PENDING.some((p) => posix.includes(p))) return false;
+ if (isTestFile(posix)) return false;
+ return true;
+}
+
+/** @type {import("eslint").Rule.RuleModule} */
+const noDefaultZoneLiteralRule = {
+ meta: {
+ type: "problem",
+ docs: {
+ description:
+ "Import DEFAULT_TIMEZONE from @/lib/tz/format instead of writing the zone out.",
+ },
+ schema: [],
+ messages: {
+ zoneLiteral:
+ 'Do not write "Europe/Berlin" out. Import DEFAULT_TIMEZONE from "@/lib/tz/format" for a fallback, or use the user\'s own zone (resolveUserTimezone / the profile timezone) for a day boundary.',
+ },
+ },
+ create(context) {
+ const filename = context.filename ?? context.getFilename?.();
+ if (!filename || !isEnforced(filename)) {
+ return {};
+ }
+ return {
+ Literal(node) {
+ if (typeof node.value === "string" && node.value.includes(ZONE)) {
+ context.report({ node, messageId: "zoneLiteral" });
+ }
+ },
+ TemplateElement(node) {
+ const raw = node.value && (node.value.cooked ?? node.value.raw);
+ if (typeof raw === "string" && raw.includes(ZONE)) {
+ context.report({ node, messageId: "zoneLiteral" });
+ }
+ },
+ JSXText(node) {
+ if (typeof node.value === "string" && node.value.includes(ZONE)) {
+ context.report({ node, messageId: "zoneLiteral" });
+ }
+ },
+ };
+ },
+};
+
+module.exports = noDefaultZoneLiteralRule;
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 36fb7684e..1399d6eee 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -101,6 +101,10 @@ const eslintConfig = defineConfig([
// catches the omission only — a handler that returns `{ ok: true }`
// after swallowing still passes, and its header says so.
"healthlog/job-handler-outcome": "error",
+ // The default zone is named once, in src/lib/tz/format.ts. A copy of the
+ // literal anywhere else is where a user outside that zone got someone
+ // else's calendar. src/lib/tz/ and test files are exempt; see the rule.
+ "healthlog/no-default-zone-literal": "error",
},
},
// v1.28.17 — every recharts-rendering component funnels through the
diff --git a/src/__tests__/date-order-explicit-formatter-guard.test.ts b/src/__tests__/date-order-explicit-formatter-guard.test.ts
index faec9324c..cf89630c0 100644
--- a/src/__tests__/date-order-explicit-formatter-guard.test.ts
+++ b/src/__tests__/date-order-explicit-formatter-guard.test.ts
@@ -259,18 +259,10 @@ const RAW_ALLOWLIST: ReadonlyArray<{ file: string; why: string }> = [
file: "src/components/measurement-reminders/vorsorge-section.tsx",
why: "The ISO day the postpone request writes to the API. A wire value, and the wire format is ISO whatever the reader prefers.",
},
- {
- file: "src/lib/gamification/achievements.ts",
- why: "The day key streak arithmetic counts on.",
- },
{
file: "src/lib/charts/bucket-time-series.ts",
why: "Bucket boundary arithmetic. The bucket LABELS are a separate step and go through `makeBucketLabelFormatters`.",
},
- {
- file: "src/lib/ai/prompts/insight-generator.ts",
- why: "An ISO day inside a model prompt. The model reads ISO; the user never sees this string.",
- },
{
file: "src/lib/jobs/measurement-reminder.ts",
why: "The local day in the notification dedup key. Changing it with a display preference would re-send a claimed reminder.",
@@ -283,10 +275,6 @@ const RAW_ALLOWLIST: ReadonlyArray<{ file: string; why: string }> = [
file: "src/lib/i18n/relative-time.ts",
why: "The en-CA day keys that decide today / yesterday. A comparison between two keys, never printed.",
},
- {
- file: "src/lib/tz/resolver.ts",
- why: "The Berlin day formatter the zone arithmetic itself is built on.",
- },
];
/**
diff --git a/src/app/api/admin/notifications/reminder-check/route.ts b/src/app/api/admin/notifications/reminder-check/route.ts
index 6cb32ae31..78b6fc9c8 100644
--- a/src/app/api/admin/notifications/reminder-check/route.ts
+++ b/src/app/api/admin/notifications/reminder-check/route.ts
@@ -9,6 +9,7 @@ import { parseScheduleRecurrence } from "@/lib/medication-schedule";
import { dispatchLocalisedNotification } from "@/lib/notifications/dispatch-localised";
import { getUserTodayBounds, getDayOfWeekInTz } from "@/lib/tz/local-day";
import { TRACKED_INTAKE_WHERE } from "@/lib/medications/intake-tracking";
+import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
export const dynamic = "force-dynamic";
@@ -116,7 +117,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
let notificationsSent = 0;
for (const med of medications) {
- const userTz = med.user.timezone || "Europe/Berlin";
+ const userTz = med.user.timezone || DEFAULT_TIMEZONE;
const { start: todayStart, end: todayEnd } = getUserTodayBounds(
now,
userTz,
diff --git a/src/app/api/coach/reminder-suggestions/route.ts b/src/app/api/coach/reminder-suggestions/route.ts
index de7a7f1ed..eaafd4844 100644
--- a/src/app/api/coach/reminder-suggestions/route.ts
+++ b/src/app/api/coach/reminder-suggestions/route.ts
@@ -44,8 +44,7 @@ import {
type ReminderScheduleInput,
} from "@/lib/measurement-reminders/scheduling";
import { toMeasurementReminderDto } from "@/lib/measurement-reminders/dto";
-
-const DEFAULT_TIMEZONE = "Europe/Berlin";
+import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
async function postAction(request: NextRequest): Promise {
const { user } = await requireAuth();
diff --git a/src/app/api/coach/suggested-actions/route.ts b/src/app/api/coach/suggested-actions/route.ts
index 8302bc33c..aa8e73959 100644
--- a/src/app/api/coach/suggested-actions/route.ts
+++ b/src/app/api/coach/suggested-actions/route.ts
@@ -47,8 +47,8 @@ import {
type ReminderScheduleInput,
} from "@/lib/measurement-reminders/scheduling";
import { toMeasurementReminderDto } from "@/lib/measurement-reminders/dto";
+import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
-const DEFAULT_TIMEZONE = "Europe/Berlin";
const CHECKUP_NOTIFY_HOUR = 9;
async function postAction(request: NextRequest): Promise {
diff --git a/src/app/api/cycle/calendar/route.ts b/src/app/api/cycle/calendar/route.ts
index 6e68e71f1..7d3b07dda 100644
--- a/src/app/api/cycle/calendar/route.ts
+++ b/src/app/api/cycle/calendar/route.ts
@@ -39,7 +39,8 @@ import {
import { resolveCycleVerdict } from "@/lib/cycle/verdict";
import { addDays, dayDiff } from "@/lib/cycle/day-math";
import { BBT_WINDOW } from "@/lib/cycle/types";
-import { DEFAULT_TIMEZONE, moodDateKey } from "@/lib/mood/date-key";
+import { moodDateKey } from "@/lib/mood/date-key";
+import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
import { getServerTranslator } from "@/lib/i18n/server-translator";
import { resolveServerLocale } from "@/lib/i18n/server-locale";
diff --git a/src/app/api/cycle/day-logs/[id]/route.ts b/src/app/api/cycle/day-logs/[id]/route.ts
index 211e07eea..4c92fe9a2 100644
--- a/src/app/api/cycle/day-logs/[id]/route.ts
+++ b/src/app/api/cycle/day-logs/[id]/route.ts
@@ -28,7 +28,7 @@ import {
ensureCycleForBleedingDay,
removeCycleStartedOn,
} from "@/lib/cycle/cycle-boundaries";
-import { DEFAULT_TIMEZONE } from "@/lib/mood/date-key";
+import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
import { encrypt, decrypt } from "@/lib/crypto";
import { getOrCreateCycleProfile } from "@/lib/cycle/profile";
import { replaceSymptomLinks } from "@/lib/cycle/day-log-write";
diff --git a/src/app/api/cycle/day-logs/bulk/route.ts b/src/app/api/cycle/day-logs/bulk/route.ts
index 93c56116b..66abea65a 100644
--- a/src/app/api/cycle/day-logs/bulk/route.ts
+++ b/src/app/api/cycle/day-logs/bulk/route.ts
@@ -40,7 +40,7 @@ import {
type UnstableExternalIdShape,
} from "@/lib/validations/external-id";
import { upsertCycleDayLog } from "@/lib/cycle/day-log-write";
-import { DEFAULT_TIMEZONE } from "@/lib/mood/date-key";
+import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
const BATCH_RATE_LIMIT_MAX = 60;
const BATCH_RATE_LIMIT_WINDOW_MS = 60 * 1000;
diff --git a/src/app/api/cycle/day-logs/route.ts b/src/app/api/cycle/day-logs/route.ts
index cacf927fd..158d85c84 100644
--- a/src/app/api/cycle/day-logs/route.ts
+++ b/src/app/api/cycle/day-logs/route.ts
@@ -40,7 +40,7 @@ import {
import { upsertCycleDayLog } from "@/lib/cycle/day-log-write";
import { findOwningCycleId } from "@/lib/cycle/cycle-attribution";
import { toCycleDayLogDTO, dayLogSymptomInclude } from "@/lib/cycle/dto";
-import { DEFAULT_TIMEZONE } from "@/lib/mood/date-key";
+import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
export const POST = apiHandler(withIdempotency<[NextRequest]>(postDayLog));
diff --git a/src/app/api/cycle/insights/route.ts b/src/app/api/cycle/insights/route.ts
index 287406817..4fc89c5b3 100644
--- a/src/app/api/cycle/insights/route.ts
+++ b/src/app/api/cycle/insights/route.ts
@@ -43,7 +43,8 @@ import {
type SymptomDay,
} from "@/lib/cycle/symptom-phase";
import { addDays } from "@/lib/cycle/day-math";
-import { DEFAULT_TIMEZONE, moodDateKey } from "@/lib/mood/date-key";
+import { moodDateKey } from "@/lib/mood/date-key";
+import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
/** Trailing window the phase-contrast walks (days). */
const WINDOW_DAYS = 365;
@@ -228,6 +229,7 @@ export const GET = apiHandler(async () => {
phaseByDay,
measurements,
userPriorityJson,
+ timeZone: tz,
});
const headline = selectHeadlinePhaseRow(rows);
// The lagged rows are served with their narrated `interpretation` intact, so
@@ -239,6 +241,7 @@ export const GET = apiHandler(async () => {
phaseByDay,
measurements,
userPriorityJson,
+ timeZone: tz,
locale,
});
diff --git a/src/app/api/cycle/period/route.ts b/src/app/api/cycle/period/route.ts
index 4fc37c6a8..80b276b3f 100644
--- a/src/app/api/cycle/period/route.ts
+++ b/src/app/api/cycle/period/route.ts
@@ -37,7 +37,7 @@ import {
toMenstrualCycleDTO,
dayLogSymptomInclude,
} from "@/lib/cycle/dto";
-import { DEFAULT_TIMEZONE } from "@/lib/mood/date-key";
+import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
import { openCycleAt, type MovedAnchors } from "@/lib/cycle/cycle-boundaries";
export const POST = apiHandler(withIdempotency<[NextRequest]>(postPeriod));
diff --git a/src/app/api/illness/episodes/[id]/correlation/route.ts b/src/app/api/illness/episodes/[id]/correlation/route.ts
index c0f647364..abc456568 100644
--- a/src/app/api/illness/episodes/[id]/correlation/route.ts
+++ b/src/app/api/illness/episodes/[id]/correlation/route.ts
@@ -22,7 +22,7 @@ import { apiSuccess, apiError } from "@/lib/api-response";
import { requireIllnessEnabled } from "@/lib/illness/gate";
import { computeEpisodeCorrelation } from "@/lib/illness/correlation-read";
import { notifyIllnessRedFlag } from "@/lib/illness/red-flag-notify";
-import { DEFAULT_TIMEZONE } from "@/lib/mood/date-key";
+import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
type RouteParams = { params: Promise<{ id: string }> };
diff --git a/src/app/api/illness/episodes/[id]/day-logs/route.ts b/src/app/api/illness/episodes/[id]/day-logs/route.ts
index 2202f338f..56b18f31d 100644
--- a/src/app/api/illness/episodes/[id]/day-logs/route.ts
+++ b/src/app/api/illness/episodes/[id]/day-logs/route.ts
@@ -34,7 +34,7 @@ import {
} from "@/lib/validations/illness";
import { upsertIllnessDayLog } from "@/lib/illness/day-log-write";
import { toIllnessDayLogDTO, dayLogSymptomInclude } from "@/lib/illness/dto";
-import { DEFAULT_TIMEZONE } from "@/lib/mood/date-key";
+import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
type RouteParams = { params: Promise<{ id: string }> };
diff --git a/src/app/api/import/route.ts b/src/app/api/import/route.ts
index 88ea23e34..e827cc674 100644
--- a/src/app/api/import/route.ts
+++ b/src/app/api/import/route.ts
@@ -37,7 +37,8 @@ import type { MeasurementType } from "@/generated/prisma/client";
import { emitInsertedMeasurementArrivals } from "@/lib/arrivals/measurement-emit";
import { maybeEnqueueMorningRefresh } from "@/lib/daily/morning-refresh-trigger";
import { isP2002, isP2025 } from "@/lib/prisma-errors";
-import { DEFAULT_TIMEZONE, moodDateKey } from "@/lib/mood/date-key";
+import { moodDateKey } from "@/lib/mood/date-key";
+import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
import { deriveA1 } from "@/lib/mood/level-a";
import { zonedWallClockToUtc } from "@/lib/tz/wall-clock";
diff --git a/src/app/api/insights/cards/route.ts b/src/app/api/insights/cards/route.ts
index b37ae9652..499171ebe 100644
--- a/src/app/api/insights/cards/route.ts
+++ b/src/app/api/insights/cards/route.ts
@@ -34,6 +34,7 @@ import {
} from "@/lib/analytics/compliance";
import type { MeasurementType } from "@/generated/prisma/client";
import { TRACKED_INTAKE_WHERE } from "@/lib/medications/intake-tracking";
+import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
type Severity = "alert" | "caution" | "info" | "good";
@@ -216,7 +217,7 @@ export const GET = apiHandler(async () => {
arr.push(e);
eventsByMed.set(e.medicationId, arr);
}
- const tz = user.timezone || "Europe/Berlin";
+ const tz = user.timezone || DEFAULT_TIMEZONE;
for (const med of activeMeds) {
const events = eventsByMed.get(med.id) ?? [];
const mapped = events.map((e) => ({
diff --git a/src/app/api/insights/comprehensive/route.ts b/src/app/api/insights/comprehensive/route.ts
index 46d6b9be7..cc55cd882 100644
--- a/src/app/api/insights/comprehensive/route.ts
+++ b/src/app/api/insights/comprehensive/route.ts
@@ -41,7 +41,7 @@ import {
ensureUserMoodRollupsFresh,
readMoodDayRollups,
} from "@/lib/rollups/mood-rollups";
-import { userDayKey } from "@/lib/tz/format";
+import { userDayKey, DEFAULT_TIMEZONE } from "@/lib/tz/format";
import { TRACKED_INTAKE_WHERE } from "@/lib/medications/intake-tracking";
export const dynamic = "force-dynamic";
@@ -125,7 +125,7 @@ export async function buildComprehensiveResponse(user: AuthedUser) {
// pass in this route (resting-pulse proxy, medication-continuity, BP
// pairing). Was previously re-derived ad hoc (or hardcoded UTC) at each
// call site.
- const userTz = user.timezone || "Europe/Berlin";
+ const userTz = user.timezone || DEFAULT_TIMEZONE;
// Fetch user profile (height + DOB drive BMI + BP targets).
const dbUser = await prisma.user.findUnique({
@@ -445,7 +445,7 @@ export async function buildComprehensiveResponse(user: AuthedUser) {
const medicationContext = buildComplianceMedicationContext(
med,
lastNonSkippedTakenAt(mapped),
- user.timezone || "Europe/Berlin",
+ user.timezone || DEFAULT_TIMEZONE,
);
const c7 = calculateCompliance(mapped, med.schedules, 7, med.createdAt, {
medicationContext,
diff --git a/src/app/api/insights/correlations/route.ts b/src/app/api/insights/correlations/route.ts
index eb4e4358e..f93c5262f 100644
--- a/src/app/api/insights/correlations/route.ts
+++ b/src/app/api/insights/correlations/route.ts
@@ -41,6 +41,7 @@ import {
} from "@/lib/insights/correlation-patterns";
import { assembleDiscoveryMatrix } from "@/lib/insights/discovery-matrix";
import { fetchLabDraws } from "@/lib/insights/correlation-channel-series";
+import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
export const dynamic = "force-dynamic";
@@ -115,7 +116,7 @@ async function buildCorrelationsResponse(
where: { id: userId },
select: { timezone: true },
});
- const tz = profile?.timezone ?? "Europe/Berlin";
+ const tz = profile?.timezone ?? DEFAULT_TIMEZONE;
const now = new Date();
const since = new Date(now.getTime() - WINDOW_DAYS * MS_PER_DAY);
diff --git a/src/app/api/insights/generate/route.ts b/src/app/api/insights/generate/route.ts
index b5706dd8b..266d77959 100644
--- a/src/app/api/insights/generate/route.ts
+++ b/src/app/api/insights/generate/route.ts
@@ -692,6 +692,7 @@ export const POST = apiHandler((request: NextRequest) =>
userId,
dbUser?.displayName ?? null,
locale,
+ await resolveUserTimezone(userId),
);
// v1.12.7 (B5) — inject the curated SOURCES block for the metric sections
diff --git a/src/app/api/measurements/route.ts b/src/app/api/measurements/route.ts
index ccc8742d6..ffcaafece 100644
--- a/src/app/api/measurements/route.ts
+++ b/src/app/api/measurements/route.ts
@@ -76,6 +76,7 @@ import type {
MeasurementSource,
GlucoseContext,
} from "@/generated/prisma/client";
+import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
export const GET = apiHandler(async (request: NextRequest) => {
const { user } = await requireRecordAuth("read", "measurements");
@@ -192,7 +193,7 @@ export const GET = apiHandler(async (request: NextRequest) => {
const tz =
user.timezone && user.timezone.length > 0
? user.timezone
- : "Europe/Berlin";
+ : DEFAULT_TIMEZONE;
// Resolve `[dayStart, dayEnd)` for the requested calendar day in
// the user's local zone. v1.4.37 W10 — `localDayWindow` honours
// DST: the previous shape (`canonicalDailyTimestamp ± 12 h`)
@@ -242,7 +243,7 @@ export const GET = apiHandler(async (request: NextRequest) => {
const tz =
user.timezone && user.timezone.length > 0
? user.timezone
- : "Europe/Berlin";
+ : DEFAULT_TIMEZONE;
// v1.29.6 — DESC + take so a long history returns the MOST RECENT
// window of raw samples for bucketing. The previous `orderBy: asc`
// + `take: limit` sliced the OLDEST page of rows before the
diff --git a/src/app/api/medications/[id]/compliance/route.ts b/src/app/api/medications/[id]/compliance/route.ts
index c53eeba65..4831388da 100644
--- a/src/app/api/medications/[id]/compliance/route.ts
+++ b/src/app/api/medications/[id]/compliance/route.ts
@@ -9,6 +9,7 @@ import {
} from "@/lib/medications/compliance-payload";
import { cachedSwr, caches, type ServerCache } from "@/lib/cache/server-cache";
import { checkRateLimit } from "@/lib/rate-limit";
+import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
type RouteParams = { params: Promise<{ id: string }> };
@@ -43,7 +44,7 @@ export const GET = apiHandler(
return apiError("Too many compliance requests. Please retry later.", 429);
}
- const userTz = user.timezone || "Europe/Berlin";
+ const userTz = user.timezone || DEFAULT_TIMEZONE;
// Read-through the per-user compliance cache (15 min fresh TTL +
// SWR window). The ownership guard and the medication read live
diff --git a/src/app/api/medications/[id]/dose-history/route.ts b/src/app/api/medications/[id]/dose-history/route.ts
index f494ed66e..0159323a1 100644
--- a/src/app/api/medications/[id]/dose-history/route.ts
+++ b/src/app/api/medications/[id]/dose-history/route.ts
@@ -41,6 +41,7 @@ import {
} from "@/lib/medications/scheduling/worker-helpers";
import { assertMedicationOwnership } from "@/lib/medications/route-guards";
import { dueSchedules } from "@/lib/medications/intake-tracking";
+import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
type RouteParams = { params: Promise<{ id: string }> };
@@ -120,7 +121,7 @@ export const GET = apiHandler(
}
const now = new Date();
- const userTz = user.timezone || "Europe/Berlin";
+ const userTz = user.timezone || DEFAULT_TIMEZONE;
const medication = await prisma.medication.findUnique({
where: { id },
diff --git a/src/app/api/medications/[id]/efficacy/route.ts b/src/app/api/medications/[id]/efficacy/route.ts
index 5bc75939d..53c54fedd 100644
--- a/src/app/api/medications/[id]/efficacy/route.ts
+++ b/src/app/api/medications/[id]/efficacy/route.ts
@@ -2,6 +2,7 @@ import { apiHandler, requireAuth } from "@/lib/api-handler";
import { apiSuccess, apiError } from "@/lib/api-response";
import { checkRateLimit } from "@/lib/rate-limit";
import { buildMedicationEfficacy } from "@/lib/medications/efficacy/build-efficacy";
+import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
type RouteParams = { params: Promise<{ id: string }> };
@@ -31,7 +32,7 @@ export const GET = apiHandler(
return apiError("Too many efficacy requests. Please retry later.", 429);
}
- const userTz = user.timezone || "Europe/Berlin";
+ const userTz = user.timezone || DEFAULT_TIMEZONE;
const dto = await buildMedicationEfficacy(user.id, id, userTz);
if (!dto) {
return apiError("Medication not found", 404);
diff --git a/src/app/api/medications/[id]/route.ts b/src/app/api/medications/[id]/route.ts
index 2d281fbff..1afbbbb26 100644
--- a/src/app/api/medications/[id]/route.ts
+++ b/src/app/api/medications/[id]/route.ts
@@ -46,6 +46,7 @@ import {
isRecordOnly,
scheduleWireFields,
} from "@/lib/medications/intake-tracking";
+import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
type RouteParams = { params: Promise<{ id: string }> };
@@ -175,7 +176,7 @@ export const GET = apiHandler(
},
schedules: liveSchedules,
now,
- userTz: user.timezone || "Europe/Berlin",
+ userTz: user.timezone || DEFAULT_TIMEZONE,
lastIntakeAt: lastIntake?.takenAt ?? null,
resolvedSlots: resolvedRows.map(toResolvedSlotMark),
eraStart: latestRevision?.validUntil ?? null,
@@ -603,7 +604,7 @@ export const PUT = apiHandler(
// `syncVersion` so delta-sync clients drop them. The projector and
// the reminder worker re-mint the anchors for the new times on their
// next pass — the route does not pre-create them.
- const userTz = user.timezone || "Europe/Berlin";
+ const userTz = user.timezone || DEFAULT_TIMEZONE;
const { start: todayStart } = getUserTodayBounds(new Date(), userTz);
const tombstoneWhere = {
userId: user.id,
diff --git a/src/app/api/medications/compliance/route.ts b/src/app/api/medications/compliance/route.ts
index 06836c731..eee5e5f97 100644
--- a/src/app/api/medications/compliance/route.ts
+++ b/src/app/api/medications/compliance/route.ts
@@ -12,6 +12,7 @@ import {
import { cachedSwr, caches, type ServerCache } from "@/lib/cache/server-cache";
import { checkRateLimit } from "@/lib/rate-limit";
import { TRACKED_INTAKE_WHERE } from "@/lib/medications/intake-tracking";
+import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
/**
* Batched card-compliance read: one round trip for every medication the
@@ -43,7 +44,7 @@ export const GET = apiHandler(async () => {
return apiError("Too many compliance requests. Please retry later.", 429);
}
- const userTz = user.timezone || "Europe/Berlin";
+ const userTz = user.timezone || DEFAULT_TIMEZONE;
// Same ordering as the medications list so the page's card order and
// this payload walk the same sequence.
diff --git a/src/app/api/mood-entries/[id]/route.ts b/src/app/api/mood-entries/[id]/route.ts
index f6535df55..94cb5f59e 100644
--- a/src/app/api/mood-entries/[id]/route.ts
+++ b/src/app/api/mood-entries/[id]/route.ts
@@ -14,7 +14,8 @@ import { NextRequest } from "next/server";
import { Prisma } from "@/generated/prisma/client";
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
-import { moodDateKey, DEFAULT_TIMEZONE } from "@/lib/mood/date-key";
+import { moodDateKey } from "@/lib/mood/date-key";
+import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
import { deriveA1, shapeLevelA } from "@/lib/mood/level-a";
import { contextForWire, persistMoodContext } from "@/lib/mood/context";
import { encryptNote, shapeMoodNote } from "@/lib/crypto/note-cipher";
diff --git a/src/app/api/mood-entries/bulk/route.ts b/src/app/api/mood-entries/bulk/route.ts
index 29bb815d7..3a504fa71 100644
--- a/src/app/api/mood-entries/bulk/route.ts
+++ b/src/app/api/mood-entries/bulk/route.ts
@@ -51,7 +51,8 @@ import {
UNSTABLE_EXTERNAL_ID_REASON,
type UnstableExternalIdShape,
} from "@/lib/validations/external-id";
-import { moodDateKey, DEFAULT_TIMEZONE } from "@/lib/mood/date-key";
+import { moodDateKey } from "@/lib/mood/date-key";
+import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
import { levelAForWrite } from "@/lib/mood/level-a";
import { persistMoodContext } from "@/lib/mood/context";
import { encryptNote } from "@/lib/crypto/note-cipher";
diff --git a/src/app/api/mood-entries/route.ts b/src/app/api/mood-entries/route.ts
index 96fb20d0a..86b14901a 100644
--- a/src/app/api/mood-entries/route.ts
+++ b/src/app/api/mood-entries/route.ts
@@ -24,7 +24,8 @@ import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import { withIdempotency } from "@/lib/idempotency";
import { encryptNote, shapeMoodNote } from "@/lib/crypto/note-cipher";
-import { moodDateKey, DEFAULT_TIMEZONE } from "@/lib/mood/date-key";
+import { moodDateKey } from "@/lib/mood/date-key";
+import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
import { levelAForWrite, shapeLevelA } from "@/lib/mood/level-a";
import { contextForWire, persistMoodContext } from "@/lib/mood/context";
import { listMoodEntriesPage } from "@/lib/mood/list-read";
diff --git a/src/app/api/mood/linked-context/route.ts b/src/app/api/mood/linked-context/route.ts
index c66e0030c..8756d58d0 100644
--- a/src/app/api/mood/linked-context/route.ts
+++ b/src/app/api/mood/linked-context/route.ts
@@ -38,8 +38,7 @@ import { apiSuccess, returnAllZodIssues } from "@/lib/api-response";
import { annotate } from "@/lib/logging/context";
import { requireModuleEnabled } from "@/lib/modules/gate";
import { resolveLinkedDayContext } from "@/lib/mood/linked-context";
-import { DEFAULT_TIMEZONE } from "@/lib/mood/date-key";
-import { isValidTimezone } from "@/lib/tz/format";
+import { DEFAULT_TIMEZONE, isValidTimezone } from "@/lib/tz/format";
export const dynamic = "force-dynamic";
diff --git a/src/app/api/sync/state/route.ts b/src/app/api/sync/state/route.ts
index b45e269a1..4d4083640 100644
--- a/src/app/api/sync/state/route.ts
+++ b/src/app/api/sync/state/route.ts
@@ -42,6 +42,7 @@ import {
NATIVE_REFRESH_TOKEN_DAYS,
TOMBSTONE_RETENTION_DAYS,
} from "@/lib/auth/native-client";
+import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
export const GET = apiHandler(async (_request: NextRequest) => {
const { user } = await requireAuth();
@@ -119,7 +120,7 @@ export const GET = apiHandler(async (_request: NextRequest) => {
return apiSuccess({
userId: user.id,
- timezone: currentUser?.timezone ?? "Europe/Berlin",
+ timezone: currentUser?.timezone ?? DEFAULT_TIMEZONE,
lastSyncedAt: lastSyncedAt?.toISOString() ?? null,
serverNow: serverNow.toISOString(),
measurements: {
diff --git a/src/app/medications/page-client.tsx b/src/app/medications/page-client.tsx
index 54e39bf6d..e37e45adf 100644
--- a/src/app/medications/page-client.tsx
+++ b/src/app/medications/page-client.tsx
@@ -48,6 +48,7 @@ import { apiGet } from "@/lib/api/api-fetch";
import { useMedicationComplianceSummaryAll } from "@/lib/queries/use-medication-compliance-summary";
import { useMedicationListLayout } from "@/lib/queries/use-medication-list-layout";
import { applyMedicationOrder } from "@/lib/medications/medication-order";
+import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
interface Schedule {
id: string;
@@ -397,7 +398,7 @@ export default function MedicationsPageClient() {
// medications past their band tail stay takeable from the batch. Page
// order is preserved so the confirm dialog lists medications as rendered.
const dueMeds = deriveDueMedications(activeMeds, {
- tz: user?.timezone || "Europe/Berlin",
+ tz: user?.timezone || DEFAULT_TIMEZONE,
thresholds: thresholds ?? undefined,
doseStatusById: new Map(
(complianceRows ?? []).map((row) => [
diff --git a/src/components/charts/medication-compliance-chart.tsx b/src/components/charts/medication-compliance-chart.tsx
index 866759999..fa5aefa0e 100644
--- a/src/components/charts/medication-compliance-chart.tsx
+++ b/src/components/charts/medication-compliance-chart.tsx
@@ -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) {
diff --git a/src/components/dashboard/medication-intake-quick-add.tsx b/src/components/dashboard/medication-intake-quick-add.tsx
index 1476bfb78..52788fd3a 100644
--- a/src/components/dashboard/medication-intake-quick-add.tsx
+++ b/src/components/dashboard/medication-intake-quick-add.tsx
@@ -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();
diff --git a/src/components/insights/intraday-pulse-chart.tsx b/src/components/insights/intraday-pulse-chart.tsx
index 965014e94..09e1ae79a 100644
--- a/src/components/insights/intraday-pulse-chart.tsx
+++ b/src/components/insights/intraday-pulse-chart.tsx
@@ -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
diff --git a/src/components/measurement-reminders/__tests__/vorsorge-section.test.tsx b/src/components/measurement-reminders/__tests__/vorsorge-section.test.tsx
index 7bba97edb..da56bb22f 100644
--- a/src/components/measurement-reminders/__tests__/vorsorge-section.test.tsx
+++ b/src/components/measurement-reminders/__tests__/vorsorge-section.test.tsx
@@ -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(" 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
diff --git a/src/components/medications/card-parts/displayed-slot-instant.ts b/src/components/medications/card-parts/displayed-slot-instant.ts
index 62eedae0d..a8d633239 100644
--- a/src/components/medications/card-parts/displayed-slot-instant.ts
+++ b/src/components/medications/card-parts/displayed-slot-instant.ts
@@ -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
diff --git a/src/components/medications/dose-history-ledger.tsx b/src/components/medications/dose-history-ledger.tsx
index e80592a7f..642e4bb7a 100644
--- a/src/components/medications/dose-history-ledger.tsx
+++ b/src/components/medications/dose-history-ledger.tsx
@@ -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<{
diff --git a/src/components/medications/glp1-medication-card.tsx b/src/components/medications/glp1-medication-card.tsx
index b0f22972e..1895daa0a 100644
--- a/src/components/medications/glp1-medication-card.tsx
+++ b/src/components/medications/glp1-medication-card.tsx
@@ -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
diff --git a/src/components/medications/log-intake-dialog.tsx b/src/components/medications/log-intake-dialog.tsx
index 111282526..411ef022f 100644
--- a/src/components/medications/log-intake-dialog.tsx
+++ b/src/components/medications/log-intake-dialog.tsx
@@ -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(
diff --git a/src/components/medications/medication-card.tsx b/src/components/medications/medication-card.tsx
index b59b2ac0b..a4d1623c3 100644
--- a/src/components/medications/medication-card.tsx
+++ b/src/components/medications/medication-card.tsx
@@ -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
diff --git a/src/components/medications/medication-table.tsx b/src/components/medications/medication-table.tsx
index d5089f611..e52c9c92a 100644
--- a/src/components/medications/medication-table.tsx
+++ b/src/components/medications/medication-table.tsx
@@ -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(initialSort);
const [, forceUpdate] = useReducer((x: number) => x + 1, 0);
diff --git a/src/components/medications/take-all-due.ts b/src/components/medications/take-all-due.ts
index aaab08810..d2ff9ac7a 100644
--- a/src/components/medications/take-all-due.ts
+++ b/src/components/medications/take-all-due.ts
@@ -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;
diff --git a/src/components/settings/account-section/index.tsx b/src/components/settings/account-section/index.tsx
index e07b9c7fa..41eb35e93 100644
--- a/src/components/settings/account-section/index.tsx
+++ b/src/components/settings/account-section/index.tsx
@@ -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(EMPTY_HEIGHT_DRAFT);
const [dateOfBirth, setDateOfBirth] = useState("");
const [gender, setGender] = useState("");
- const [timezone, setTimezone] = useState("Europe/Berlin");
+ const [timezone, setTimezone] = useState(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: "",
diff --git a/src/components/settings/managed-record-settings-section.tsx b/src/components/settings/managed-record-settings-section.tsx
index 72639967c..fe98c4f5a 100644
--- a/src/components/settings/managed-record-settings-section.tsx
+++ b/src/components/settings/managed-record-settings-section.tsx
@@ -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({