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;