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({