feat(vorsorge): resolve reminders on ingest, not only on the cron

Add a reminder-satisfy pg-boss queue (mirrors pr-detection) that every
measurement and lab ingest path enqueues — manual create, batch, and the
Withings / Polar / Oura / Nightscout / Fitbit / WHOOP syncs. A reminder
now auto-resolves the moment a matching reading lands instead of waiting
up to 15 minutes. The 15-minute cron stays as the idempotent safety net;
the forward-only guard lets the eventful hook and the cron converge
without double-stamping.

The worker honours the module toggle (a reminder for a disabled module
produces no engine activity) and runs the same satisfaction primitives
the cron uses.
This commit is contained in:
Marc-André Bombeck committed 2026-06-16 20:58:56 +02:00
1 parent f92247760c
commit 7b57c1d3d8
14 files changed
+389 -5

No files matched your search

+6
View File
@@ -11,6 +11,7 @@ import {
import { auditLog } from "@/lib/auth/audit";
import { prisma } from "@/lib/db";
import { withIdempotency } from "@/lib/idempotency";
import { enqueueReminderSatisfy } from "@/lib/jobs/reminder-satisfy";
import { encryptNoteToBytes } from "@/lib/labs/store";
import { annotate } from "@/lib/logging/context";
import {
@@ -184,5 +185,10 @@ async function postLabResult(request: NextRequest) {
meta: { labResultId: created.id },
});
// v1.18.1 (D2) — eventful Lab↔Vorsorge satisfaction. A lab panel just
// landed; resolve the user's free-text "annual blood panel" reminders
// now rather than waiting on the 15-min cron. Fire-and-forget.
void enqueueReminderSatisfy(user.id).catch(() => {});
return apiSuccess(serialiseLabResult(created), 201);
}
+5
View File
@@ -52,6 +52,7 @@ import { validateMeasurementRange } from "@/lib/validations/measurement";
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 { invalidateUserMeasurements } from "@/lib/cache/invalidate";
import { invalidateStatusInsightsForTypes } from "@/lib/insights/comprehensive-generate";
import {
@@ -599,6 +600,10 @@ async function postBatch(request: NextRequest): Promise<Response> {
// personal record.
if (insertedCount > 0 || updatedCount > 0 || duplicateCount > 0) {
const silent = entries.length > PR_DETECTION_SILENT_THRESHOLD;
// v1.18.1 — eventful Vorsorge satisfaction. A matching reading just
// landed; resolve the user's reminders now rather than waiting on the
// 15-min cron. Fire-and-forget — the cron reconciles on enqueue miss.
void enqueueReminderSatisfy(user.id).catch(() => {});
try {
await enqueuePrDetection(user.id, { silent });
await auditLog("personal_records.detection_enqueued", {
+9
View File
@@ -29,6 +29,7 @@ import {
import { withIdempotency } from "@/lib/idempotency";
import { invalidateUserMeasurements } from "@/lib/cache/invalidate";
import { invalidateStatusInsightsForTypes } from "@/lib/insights/comprehensive-generate";
import { enqueueReminderSatisfy } from "@/lib/jobs/reminder-satisfy";
import {
recomputeBucketsForMeasurement,
collapseToTypeDayKeys,
@@ -924,6 +925,10 @@ async function postMeasurement(request: NextRequest) {
// SWR readers don't serve the pre-write body back to the user.
invalidateUserMeasurements(user.id, { evict: true });
// v1.18.1 — eventful Vorsorge satisfaction. Resolve the user's
// reminders against the just-landed readings now. Fire-and-forget.
void enqueueReminderSatisfy(user.id).catch(() => {});
// v1.5.0 — refresh the persistent rollup table for every distinct
// (type, day) the batch touched so the next analytics / coach read
// hits the cache rather than falling through to live aggregation.
@@ -1037,6 +1042,10 @@ async function postMeasurement(request: NextRequest) {
// pre-write body back to the user.
invalidateUserMeasurements(user.id, { evict: true });
// v1.18.1 — eventful Vorsorge satisfaction. Resolve the user's reminders
// against the just-landed reading now. Fire-and-forget.
void enqueueReminderSatisfy(user.id).catch(() => {});
// v1.5.0 — refresh the persistent rollup row for the affected
// (type, day) tuple. Runs inline so the next read of the
// comprehensive / analytics surface is correct without waiting on