mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
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:
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;
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user