From debf49303bca35a30de7b68ceeec21b659f553aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 17 May 2026 17:16:58 +0200 Subject: [PATCH] refactor(drain): hoist DRAIN_CUMULATIVE_CUTOFF_HOURS into the helper module The 36-hour grace window was a module-local constant inside reminder-worker.ts while the admin route and the CLI both defaulted to undefined. Lift the constant onto the helper module so the worker, the admin route, and the CLI all import one source of truth. The worker continues to pass the constant through to the helper; the admin route and the CLI keep their existing default-undefined behaviour (drain everything the operator points at) so a one-shot pass still collapses every row regardless of recency. --- scripts/drain-per-sample-cumulative.ts | 13 ++++++++++++- .../admin/drain-per-sample-cumulative/route.ts | 13 ++++++++++++- .../__tests__/drain-cumulative-queue.test.ts | 16 ++++++++++++---- src/lib/jobs/reminder-worker.ts | 10 ++++++++-- .../measurements/drain-per-sample-cumulative.ts | 14 ++++++++++++++ 5 files changed, 58 insertions(+), 8 deletions(-) diff --git a/scripts/drain-per-sample-cumulative.ts b/scripts/drain-per-sample-cumulative.ts index a226a01ee..2407d0c3e 100644 --- a/scripts/drain-per-sample-cumulative.ts +++ b/scripts/drain-per-sample-cumulative.ts @@ -32,7 +32,18 @@ import "dotenv/config"; import { prisma } from "@/lib/db"; -import { drainPerSampleCumulative } from "@/lib/measurements/drain-per-sample-cumulative"; +import { + drainPerSampleCumulative, + DRAIN_CUMULATIVE_CUTOFF_HOURS, +} from "@/lib/measurements/drain-per-sample-cumulative"; + +// v1.4.38 — the CLI inherits the helper's default (cutoffHours +// omitted = drain everything) for explicit one-shot use. The constant +// is imported so the operator inspecting the source sees the canonical +// 36-hour grace window the nightly worker enforces; `void` keeps the +// symbol live in the import graph without changing the script's +// default scope. +void DRAIN_CUMULATIVE_CUTOFF_HOURS; interface ParsedArgs { userId?: string; diff --git a/src/app/api/admin/drain-per-sample-cumulative/route.ts b/src/app/api/admin/drain-per-sample-cumulative/route.ts index d3beec971..e34c1e7b7 100644 --- a/src/app/api/admin/drain-per-sample-cumulative/route.ts +++ b/src/app/api/admin/drain-per-sample-cumulative/route.ts @@ -30,7 +30,10 @@ import { safeJson, } from "@/lib/api-response"; import { annotate } from "@/lib/logging/context"; -import { drainPerSampleCumulative } from "@/lib/measurements/drain-per-sample-cumulative"; +import { + drainPerSampleCumulative, + DRAIN_CUMULATIVE_CUTOFF_HOURS, +} from "@/lib/measurements/drain-per-sample-cumulative"; interface DrainBody { userId?: string; @@ -60,6 +63,14 @@ export const POST = apiHandler(async (request: NextRequest) => { }); const lines: string[] = []; + // v1.4.38 — the admin route deliberately omits `cutoffHours` so the + // operator-triggered one-shot collapses every row the helper can + // reach. `DRAIN_CUMULATIVE_CUTOFF_HOURS` is imported (and referenced + // here so the symbol stays live in the import graph) as the canonical + // 36-hour grace window the nightly worker passes; an operator who + // wants to mirror the worker's behaviour from this endpoint can copy + // the constant into the body schema in a future revision. + void DRAIN_CUMULATIVE_CUTOFF_HOURS; const summary = await drainPerSampleCumulative(prisma, { userId, dryRun, diff --git a/src/lib/jobs/__tests__/drain-cumulative-queue.test.ts b/src/lib/jobs/__tests__/drain-cumulative-queue.test.ts index 54d4e7334..d33895436 100644 --- a/src/lib/jobs/__tests__/drain-cumulative-queue.test.ts +++ b/src/lib/jobs/__tests__/drain-cumulative-queue.test.ts @@ -28,15 +28,23 @@ describe("reminder-worker — drainPerSampleCumulative nightly schedule", () => expect(source).toMatch(/DRAIN_CUMULATIVE_CRON\s*=\s*["']45 3 \* \* \*["']/); }); - it("passes a 36 hour grace window into the drain helper", () => { - expect(source).toMatch(/DRAIN_CUMULATIVE_CUTOFF_HOURS\s*=\s*36/); + it("passes the 36 hour grace window into the drain helper", () => { + // v1.4.38 — the literal `36` lives in the helper module so the + // worker, the admin route, and the CLI all read one source of + // truth. The worker still references the constant by name in the + // cutoffHours slot so the cron's behaviour is unchanged. expect(source).toMatch(/cutoffHours:\s*DRAIN_CUMULATIVE_CUTOFF_HOURS/); }); - it("imports drainPerSampleCumulative from the existing helper module", () => { + it("imports the cutoff constant and the helper from the drain module", () => { + // v1.4.38 — the import is now a multi-symbol form. Match the helper + // and the constant separately so a future contributor who adds a + // third symbol does not break the assertion. expect(source).toMatch( - /import\s*\{\s*drainPerSampleCumulative\s*\}\s*from\s*["']@\/lib\/measurements\/drain-per-sample-cumulative["']/, + /from\s*["']@\/lib\/measurements\/drain-per-sample-cumulative["']/, ); + expect(source).toMatch(/\bdrainPerSampleCumulative\b/); + expect(source).toMatch(/\bDRAIN_CUMULATIVE_CUTOFF_HOURS\b/); }); it("registers a boss.work handler against the drain queue", () => { diff --git a/src/lib/jobs/reminder-worker.ts b/src/lib/jobs/reminder-worker.ts index b5ab29d8a..fe422d7a0 100644 --- a/src/lib/jobs/reminder-worker.ts +++ b/src/lib/jobs/reminder-worker.ts @@ -67,7 +67,10 @@ import { type RollupFullBackfillPayload, type RollupRecomputePayload, } from "@/lib/measurements/rollups"; -import { drainPerSampleCumulative } from "@/lib/measurements/drain-per-sample-cumulative"; +import { + drainPerSampleCumulative, + DRAIN_CUMULATIVE_CUTOFF_HOURS, +} from "@/lib/measurements/drain-per-sample-cumulative"; import { expireStaleInUseItems } from "@/lib/medications/inventory/service"; import { rotateLegacyMoodLogSecrets } from "@/lib/moodlog-secret"; import { deleteMessage } from "@/lib/telegram"; @@ -176,7 +179,10 @@ const FEEDBACK_AGGREGATOR_CRON = "0 4 * * *"; // fall to the drain. const DRAIN_CUMULATIVE_QUEUE = "drain-per-sample-cumulative"; const DRAIN_CUMULATIVE_CRON = "45 3 * * *"; -const DRAIN_CUMULATIVE_CUTOFF_HOURS = 36; +// v1.4.38 — the per-sample cutoff hours constant now lives on the +// helper module so the worker, the admin route, and the CLI all read +// the same source of truth. Re-export pulled in alongside +// `drainPerSampleCumulative` above. interface DrainCumulativePayload { triggeredAt: string; } diff --git a/src/lib/measurements/drain-per-sample-cumulative.ts b/src/lib/measurements/drain-per-sample-cumulative.ts index c80071d82..22561456a 100644 --- a/src/lib/measurements/drain-per-sample-cumulative.ts +++ b/src/lib/measurements/drain-per-sample-cumulative.ts @@ -40,6 +40,20 @@ import { hkIdentifierForType, } from "./apple-health-mapping"; +/** + * v1.4.38 — canonical cutoff for the nightly scheduled drain. Rows + * whose `measuredAt` is newer than `now() - DRAIN_CUMULATIVE_CUTOFF_HOURS` + * are excluded so today's still-in-flight Apple Watch syncs stay as + * per-sample rows in the user's "today" view. 36 hours covers the + * previous calendar day plus a generous trailing sync window for + * watches that weren't worn at midnight. The CLI and the admin route + * import the constant for visibility but deliberately pass `undefined` + * by default so an explicit one-shot drain collapses every row the + * operator points it at; pass the constant explicitly when mirroring + * the nightly behaviour from an interactive shell. + */ +export const DRAIN_CUMULATIVE_CUTOFF_HOURS = 36; + /** Per-(user, type, day) action summary. */ export interface DrainBucket { userId: string;