refactor(tz): take the medication and reminder trees off the zone literal

The medication and measurement-reminder code now imports DEFAULT_TIMEZONE
from the tz module like every other fallback, so the lint rule no longer
exempts those paths.
This commit is contained in:
Marc-André Bombeck committed 2026-09-26 17:03:19 +02:00
1 parent e2814336e5
commit 314e9c1aa3
13 files changed
+29 -42

No files matched your search

@@ -1,7 +1,6 @@
/**
* 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.
* banned in application source and allowed only in `src/lib/tz/` and in tests.
*/
import { describe, it } from "vitest";
import { RuleTester } from "eslint";
@@ -40,17 +39,23 @@ ruleTester.run("no-default-zone-literal", rule, {
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: [
{
// The medication and reminder trees carry no exemption.
code: 'const DEFAULT_TZ = "Europe/Berlin";',
filename: "/repo/src/lib/medications/window-status.ts",
errors: [{ messageId: "zoneLiteral" }],
},
{
code: 'const tz = timezone || "Europe/Berlin";',
filename: "/repo/src/app/api/measurement-reminders/route.ts",
errors: [{ messageId: "zoneLiteral" }],
},
{
code: 'const tz = user.timezone || "Europe/Berlin";',
filename: "/repo/src/app/api/insights/cards/route.ts",
@@ -19,8 +19,6 @@
* 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";
@@ -29,14 +27,6 @@ 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, "/");
}
@@ -54,7 +44,6 @@ 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;
}
@@ -30,11 +30,10 @@ import { annotate } from "@/lib/logging/context";
import { satisfyReminder } from "@/lib/measurement-reminders/satisfy";
import { toMeasurementReminderDto } from "@/lib/measurement-reminders/dto";
import { isScreeningReminderType } from "@/lib/validations/measurement-reminders";
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
type RouteParams = { params: Promise<{ id: string }> };
const DEFAULT_TIMEZONE = "Europe/Berlin";
export const POST = apiHandler(
async (request: NextRequest, { params }: RouteParams) => {
// v1.37.0 — MANAGE. Marking a reminder satisfied re-anchors a schedule the level
@@ -33,13 +33,11 @@ import {
} from "@/lib/measurement-reminders/scheduling";
import { toMeasurementReminderDto } from "@/lib/measurement-reminders/dto";
import { wallClockInTz, zonedWallClockToUtc } from "@/lib/tz/wall-clock";
import { userDayKey } from "@/lib/tz/format";
import { DEFAULT_TIMEZONE, userDayKey } from "@/lib/tz/format";
import { holdsOpenAfterReminder } from "@/lib/measurement-reminders/holds-open";
type RouteParams = { params: Promise<{ id: string }> };
const DEFAULT_TIMEZONE = "Europe/Berlin";
async function resolveTimezone(userId: string): Promise<string> {
const row = await prisma.user.findUnique({
where: { id: userId },
@@ -16,11 +16,10 @@ import { annotate } from "@/lib/logging/context";
import { satisfyReminder } from "@/lib/measurement-reminders/satisfy";
import { toMeasurementReminderDto } from "@/lib/measurement-reminders/dto";
import { isScreeningReminderType } from "@/lib/validations/measurement-reminders";
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
type RouteParams = { params: Promise<{ id: string }> };
const DEFAULT_TIMEZONE = "Europe/Berlin";
export const POST = apiHandler(
async (request: NextRequest, { params }: RouteParams) => {
// v1.37.0 — MANAGE. The same primitive from the checklist side re-anchors a schedule the level
@@ -22,11 +22,10 @@ import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
import { annotate } from "@/lib/logging/context";
import { skipReminder } from "@/lib/measurement-reminders/satisfy";
import { toMeasurementReminderDto } from "@/lib/measurement-reminders/dto";
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
type RouteParams = { params: Promise<{ id: string }> };
const DEFAULT_TIMEZONE = "Europe/Berlin";
export const POST = apiHandler(
async (request: NextRequest, { params }: RouteParams) => {
// MANAGE — the same level as satisfy/complete: deciding to let a cycle
@@ -29,11 +29,10 @@ import { annotate } from "@/lib/logging/context";
import { toMeasurementReminderDto } from "@/lib/measurement-reminders/dto";
import { snoozeMeasurementReminderSchema } from "@/lib/validations/measurement-reminders";
import { zonedWallClockToUtc } from "@/lib/tz/wall-clock";
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
type RouteParams = { params: Promise<{ id: string }> };
const DEFAULT_TIMEZONE = "Europe/Berlin";
/** Furthest a snooze may reach: five years from now. */
const SNOOZE_CAP_MS = 5 * 365.25 * 24 * 60 * 60 * 1000;
+1 -2
View File
@@ -25,8 +25,7 @@ import {
} from "@/lib/measurement-reminders/scheduling";
import { toMeasurementReminderDto } from "@/lib/measurement-reminders/dto";
import { listMeasurementReminders } from "@/lib/measurement-reminders/list-read";
const DEFAULT_TIMEZONE = "Europe/Berlin";
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
async function resolveTimezone(userId: string): Promise<string> {
const row = await prisma.user.findUnique({
+4 -3
View File
@@ -59,6 +59,7 @@ import { satisfyReminder } from "@/lib/measurement-reminders/satisfy";
import { evaluateCoachContextReminders } from "@/lib/ai/coach/context-reminders";
import { calendarDaysUntil } from "@/lib/measurement-reminders/due-day";
import { holdsOpenAfterReminder } from "@/lib/measurement-reminders/holds-open";
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
/**
* v1.18.0 — map a reminder's `measurementType` to the toggleable module
@@ -231,7 +232,7 @@ export function evaluateMeasurementReminderDue(
if (!reminder.enabled || reminder.nextDueAt === null) {
return { fire: false, inHourWindow: false, isDue: false };
}
const tz = timezone || "Europe/Berlin";
const tz = timezone || DEFAULT_TIMEZONE;
const isDue = now.getTime() >= reminder.nextDueAt.getTime();
const parts = wallClockInTz(now, tz);
const inHourWindow = parts.hour === reminderSlotHour(reminder, tz);
@@ -326,7 +327,7 @@ export async function runMeasurementReminderTick(
for (const reminder of reminders) {
summary.candidatesScanned += 1;
try {
const timezone = reminder.user.timezone || "Europe/Berlin";
const timezone = reminder.user.timezone || DEFAULT_TIMEZONE;
// v1.39.2 — a reminder already sent for its current slot stays in
// this scan every tick until it is satisfied. Outside its notify hour
@@ -587,7 +588,7 @@ export async function runReminderSatisfyForUser(
where: { id: userId },
select: { timezone: true },
});
const timezone = user?.timezone || "Europe/Berlin";
const timezone = user?.timezone || DEFAULT_TIMEZONE;
const reminders = await prisma.measurementReminder.findMany({
where: { userId, deletedAt: null, enabled: true },
+2 -1
View File
@@ -30,6 +30,7 @@ import {
nextOccurrenceAfter,
} from "@/lib/medications/scheduling/recurrence";
import { wallClockInTz } from "@/lib/tz/wall-clock";
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
/**
* The reminder fields this module reads. A subset of the Prisma row so
@@ -133,7 +134,7 @@ export function buildReminderRecurrence(
// `anchorDate` / `endsOn` are instants instead (the form sends local
// midnight, a Coach course sends `now + days`), so hand the engine the
// calendar day each instant falls on in the user's zone.
const tz = timeZone || "Europe/Berlin";
const tz = timeZone || DEFAULT_TIMEZONE;
const startsOn = calendarDayInZone(
reminder.anchorDate ?? reminder.createdAt,
tz,
+1 -2
View File
@@ -2,6 +2,7 @@ import {
reduceCurrentWindowStatus,
type ScheduleWindowInput,
} from "@/lib/medications/window-status";
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
export interface DefaultMedicationOption {
id: string;
@@ -24,8 +25,6 @@ const DEFAULT_THRESHOLDS: MedicationDueThresholds = {
missedMinutes: 240,
};
const DEFAULT_TIMEZONE = "Europe/Berlin";
/**
* Pick the medication that should be pre-selected when an intake form opens.
* Current, late, and overdue schedule windows win; otherwise the fallback is
+2 -1
View File
@@ -35,6 +35,7 @@ import {
} from "@/lib/medications/intake-tracking";
import { liveEraStartsByMedication } from "@/lib/medications/scheduling/live-era";
import { floorWhole } from "@/lib/medications/units-per-dose";
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
export type MedicationsListResult = Array<Record<string, unknown>>;
@@ -327,7 +328,7 @@ export async function readMedicationsListCached(user: {
id: string;
timezone: string | null;
}): Promise<MedicationsListResult> {
const userTz = user.timezone || "Europe/Berlin";
const userTz = user.timezone || DEFAULT_TIMEZONE;
return cachedSwr(
caches.medications as ServerCache<MedicationsListResult>,
user.id,
+2 -4
View File
@@ -48,6 +48,7 @@ import {
wallClockInTz,
type WallClockParts,
} from "@/lib/tz/wall-clock";
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
/** One explicit per-dose on-time window as the API serialises it. */
export interface DoseWindowEntryInput {
@@ -123,9 +124,6 @@ export interface NextDueGate {
overdue: boolean;
}
/** Fallback timezone until every call site threads the user's own. */
const DEFAULT_TZ = "Europe/Berlin";
/**
* Minute-of-day an observer in the target zone reads off the wall clock.
* Every band comparison below is minute-of-day arithmetic, so this is the
@@ -516,7 +514,7 @@ export function reduceCurrentWindowStatus<
active,
lastTakenAt,
todayEventCount,
tz = DEFAULT_TZ,
tz = DEFAULT_TIMEZONE,
nextDue,
} = options;
const nowParts = wallClockInTz(now, tz);