fix(environment): end the nightly window on the person's today

The default lookback ended on the UTC date, so east of UTC the local today
stayed out of the window until the next night's run.
This commit is contained in:
Marc-André Bombeck committed 2026-10-20 09:58:02 +02:00
1 parent 2372fee376
commit 2f32a4458c
2 files changed
+57 -15

No files matched your search

@@ -0,0 +1,35 @@
/**
* The nightly environment window ends on the person's today, not the UTC
* date: east of UTC the local day begins hours before the UTC one.
*/
import { describe, expect, it, vi } from "vitest";
vi.mock("@/lib/db", () => ({ prisma: {} }));
import { resolveRange } from "../environment-fetch";
describe("resolveRange", () => {
// 20:00 UTC on 1 March is already 2 March in Auckland.
const now = new Date("2026-03-01T20:00:00.000Z");
it("ends the lookback on the local today", () => {
expect(resolveRange({}, "Pacific/Auckland", now)).toEqual({
startDate: "2026-02-23",
endDate: "2026-03-02",
});
expect(resolveRange({}, "America/Los_Angeles", now)).toEqual({
startDate: "2026-02-22",
endDate: "2026-03-01",
});
});
it("keeps an explicit range as given", () => {
expect(
resolveRange(
{ startDate: "2025-01-01", endDate: "2025-01-31" },
"Pacific/Auckland",
now,
),
).toEqual({ startDate: "2025-01-01", endDate: "2025-01-31" });
});
});
+22 -15
View File
@@ -39,8 +39,9 @@ import {
ENVIRONMENT_MAX_BACKFILL_DAYS,
fetchAndStoreEnvironment,
fillAirQualityGaps,
utcDayKey,
} from "@/lib/environment/service";
import { shiftDateKey, userDayKey } from "@/lib/tz/format";
import { resolveUserTimezone } from "@/lib/tz/resolver";
import { enumerateDayCount } from "@/lib/environment/day-span";
import { recordError } from "@/lib/jobs/worker-status";
import { jobDone, jobFailed, type JobOutcome } from "@/lib/jobs/job-outcome";
@@ -54,8 +55,6 @@ export const ENVIRONMENT_FETCH_CRON = "10 2 * * *";
export const ENVIRONMENT_FETCH_CONCURRENCY = 1;
const MS_PER_DAY = 24 * 60 * 60 * 1000;
/**
* Per-user fetch payload. An empty payload (no `userId`) is the daily discovery
* tick. `startDate`/`endDate` (YYYY-MM-DD) drive the on-demand backfill; omitted
@@ -67,18 +66,23 @@ export interface EnvironmentFetchPayload {
endDate?: string;
}
/** Resolve the date range for a per-user run (explicit range, else lookback). */
function resolveRange(payload: EnvironmentFetchPayload): {
startDate: string;
endDate: string;
} {
const today = new Date();
const endDate = payload.endDate ?? utcDayKey(today);
/**
* Resolve the date range for a per-user run (explicit range, else lookback).
*
* The lookback ends on the person's today, in the zone the service keys the
* stored days in, not on the UTC date: east of UTC the local day starts up to
* fourteen hours before the UTC one, and a window ending on the UTC date left
* the local today out until the next night's run.
*/
export function resolveRange(
payload: EnvironmentFetchPayload,
tz: string,
now: Date = new Date(),
): { startDate: string; endDate: string } {
const today = userDayKey(now, tz);
const endDate = payload.endDate ?? today;
const startDate =
payload.startDate ??
utcDayKey(
new Date(today.getTime() - ENVIRONMENT_LOOKBACK_DAYS * MS_PER_DAY),
);
payload.startDate ?? shiftDateKey(today, -ENVIRONMENT_LOOKBACK_DAYS);
return { startDate, endDate };
}
@@ -208,7 +212,10 @@ export async function handleEnvironmentFetch(
return jobDone({ skipped: "environment_module_disabled" });
}
const { startDate, endDate } = resolveRange(payload);
const { startDate, endDate } = resolveRange(
payload,
await resolveUserTimezone(userId),
);
// Clamp the span so a crafted backfill can never fan out an unbounded range.
const span = enumerateDayCount(startDate, endDate);
if (span > ENVIRONMENT_MAX_BACKFILL_DAYS) {