mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
feat(daily): finalise the morning digest when last night's sleep lands
The nightly insight pre-pass runs ~04:30, before last night's sleep has synced, so the morning digest and health score reference the day without the current sleep. Close the gap the event-driven way instead of an on-mount generation. A day's digest is now provisional until the sleep-dependent output has been regenerated with last night's sleep, and final after. The lifecycle is driven by a real marker (User.morningDigestRefreshedOn, the user-local date the refresh completed), not a heuristic: it flips the instant the refresh runs, ahead of the snapshot cache that feeds the sleep last-seen fallback. A day with no sleep device stays honestly provisional and keeps the sleep-pending note rather than showing a stale final score. When a completed sleep segment for last night lands from any of the three sleep transports — Withings sync-sleep, the WHOOP sync, the Apple-Health measurement batch — a debounced morning-digest-refresh job is enqueued. "Last night" is judged in the user's profile timezone, so a full-history re-sync never re-triggers for an old night. The queue singletonKey plus the marker collapse the many samples of one night, across all sources, into one refresh per user per local morning; the job re-runs the same comprehensive pipeline the nightly cron uses (forced, so the content-hash gate turns a no-change regen into a cheap no-op) and stamps the marker. A hard generation failure leaves the day provisional and hands recovery to the existing 45-minute briefing-retry machinery.
This commit is contained in:
1 parent
b7a6b4c898
commit
b6ded4d1f9
14 files changed
+790
-5
No files matched your search
@@ -64,6 +64,7 @@ import { deviceTypeEnum } from "@/lib/validations/source-priority";
|
||||
import { checkRateLimit } from "@/lib/rate-limit";
|
||||
import { enqueuePrDetection } from "@/lib/jobs/pr-detection";
|
||||
import { enqueueReminderSatisfy } from "@/lib/jobs/reminder-satisfy";
|
||||
import { maybeEnqueueMorningRefresh } from "@/lib/daily/morning-refresh-trigger";
|
||||
import { invalidateUserMeasurements } from "@/lib/cache/invalidate";
|
||||
import { invalidateStatusInsightsForTypes } from "@/lib/insights/comprehensive-generate";
|
||||
import {
|
||||
@@ -764,6 +765,22 @@ async function postBatch(request: NextRequest): Promise<Response> {
|
||||
} catch (err) {
|
||||
console.warn("[measurements] rollup recompute failed", err);
|
||||
}
|
||||
|
||||
// S4 — Apple-Health sleep is the third sleep transport. If any SLEEP_DURATION
|
||||
// row for last night just landed, kick the debounced morning refresh so the
|
||||
// digest/score finalise with the current sleep. The trigger judges "last
|
||||
// night" in the user's profile tz, so a historical Apple export replaying
|
||||
// months of nights never re-triggers for an old night. Fire-and-forget.
|
||||
void maybeEnqueueMorningRefresh(
|
||||
user.id,
|
||||
prepared
|
||||
.filter((p) => p.row.type === "SLEEP_DURATION")
|
||||
.map((p) =>
|
||||
p.row.measuredAt instanceof Date
|
||||
? p.row.measuredAt
|
||||
: new Date(p.row.measuredAt as string),
|
||||
),
|
||||
).catch(() => {});
|
||||
}
|
||||
|
||||
return apiSuccess({
|
||||
|
||||
Reference in new issue
Block a user