mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 06:32:03 +00:00
fix(tz): one default zone, and the user's own calendar where Berlin was hard-coded
The status assessments folded their recent days, weeks, months and years on Berlin days whatever the user's zone, the mood and cycle crosstabs keyed their metric days in Berlin, the resting-pulse proxy defaulted to Berlin days, the briefing's opener rotated at Berlin midnight, and every assessment prompt labelled today's date as Berlin's. They now use the user's zone. Rollup months and years are labelled by their UTC start, which is how they are cut; read in a zone west of UTC the label named the month before. "Europe/Berlin" was written out in some eighty places. DEFAULT_TIMEZONE in lib/tz/format is now the one definition: the second copy in mood/date-key, DISPLAY_TIMEZONE, the Berlin-only day key helpers and the per-route copies are gone. An in-repo lint rule rejects the literal outside lib/tz and tests; four areas another change owns are listed as pending.
This commit is contained in:
1 parent
c366d4946c
commit
9d2fd1db14
124 files changed
+705
-415
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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<Response> {
|
||||
const { user } = await requireAuth();
|
||||
|
||||
@@ -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<Response> {
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 }> };
|
||||
|
||||
|
||||
@@ -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 }> };
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in new issue
Block a user