From 926ac36deab6c49127ff3f67df768243d14b7879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 21 May 2026 12:40:37 +0200 Subject: [PATCH] fix(insights): exclude soft-deleted measurements in remaining measurement.findMany sites W-DELETED closed the dashboard, analytics, summaries and rollup tiers but the Insights cluster (tile-strip, AI Coach snapshot, GLP-1 plateau detector, pulse-status helper) still walked `prisma.measurement.findMany` without filtering tombstoned rows. Once iOS sync starts emitting deletions any soft-deleted reading would still contribute to: - /api/insights/targets 30-day average + 1-year latest-ever tile values (recentMeasurements, latestEverByType, glucoseRows) - /api/insights/cards 90-day weight/BP/pulse window the iOS-adapter card stream consumes - /api/insights/generate per-type aggregate payload feeding the AI prompt - Coach prompt feature aggregator (lib/insights/features.ts) - GLP-1 plateau detection window (lib/insights/glp1-plateau.ts) - Pulse-status snapshot input (lib/insights/pulse-status.ts) Add `deletedAt: null` to every remaining production `where` clause. `comprehensive-aggregator.ts` already filtered; `ai/coach/snapshot.ts` already filtered. Closes senior H1 from the v1.4.40 QA round. --- src/app/api/insights/cards/route.ts | 1 + src/app/api/insights/generate/route.ts | 2 +- src/app/api/insights/targets/route.ts | 7 ++++++- src/lib/insights/features.ts | 4 ++-- src/lib/insights/glp1-plateau.ts | 1 + src/lib/insights/pulse-status.ts | 1 + 6 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/app/api/insights/cards/route.ts b/src/app/api/insights/cards/route.ts index f070aa305..dd7052852 100644 --- a/src/app/api/insights/cards/route.ts +++ b/src/app/api/insights/cards/route.ts @@ -77,6 +77,7 @@ export const GET = apiHandler(async () => { in: ["WEIGHT", "BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA", "PULSE"], }, measuredAt: { gte: ninetyDaysAgo }, + deletedAt: null, }, orderBy: { measuredAt: "asc" }, select: { type: true, value: true, measuredAt: true }, diff --git a/src/app/api/insights/generate/route.ts b/src/app/api/insights/generate/route.ts index 96c7e3413..eee951223 100644 --- a/src/app/api/insights/generate/route.ts +++ b/src/app/api/insights/generate/route.ts @@ -134,7 +134,7 @@ async function buildComparisonSnapshotForUser( const rows = await Promise.all( types.map(async (type) => { const measurements = await prisma.measurement.findMany({ - where: { userId, type }, + where: { userId, type, deletedAt: null }, orderBy: { measuredAt: "asc" }, select: { measuredAt: true, value: true }, }); diff --git a/src/app/api/insights/targets/route.ts b/src/app/api/insights/targets/route.ts index c613fbd32..46e195659 100644 --- a/src/app/api/insights/targets/route.ts +++ b/src/app/api/insights/targets/route.ts @@ -178,11 +178,15 @@ async function buildTargetsResponse(user: AuthedUser) { const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); // Fetch all measurements in the last 30 days + the latest for each type + // Soft-delete filter mirrors the W-DELETED pattern: tombstoned rows + // must not contribute to tile averages / latest values once iOS sync + // starts emitting deletions. const recentMeasurements = await prisma.measurement.findMany({ where: { userId, type: { in: types }, measuredAt: { gte: thirtyDaysAgo }, + deletedAt: null, }, orderBy: { measuredAt: "desc" }, select: { type: true, value: true, measuredAt: true }, @@ -207,6 +211,7 @@ async function buildTargetsResponse(user: AuthedUser) { userId, type: { in: types }, measuredAt: { gte: oneYearAgo }, + deletedAt: null, }, orderBy: { measuredAt: "desc" }, distinct: ["type"], @@ -1193,7 +1198,7 @@ async function buildTargetsResponse(user: AuthedUser) { }; const thirtyDaysAgoGlucose = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); const glucoseRows = await prisma.measurement.findMany({ - where: { userId, type: "BLOOD_GLUCOSE" }, + where: { userId, type: "BLOOD_GLUCOSE", deletedAt: null }, orderBy: { measuredAt: "desc" }, select: { value: true, measuredAt: true, glucoseContext: true }, }); diff --git a/src/lib/insights/features.ts b/src/lib/insights/features.ts index b77b53d52..d0e168481 100644 --- a/src/lib/insights/features.ts +++ b/src/lib/insights/features.ts @@ -377,8 +377,8 @@ export async function extractFeatures( : null; const measurements = await prisma.measurement.findMany({ where: sinceCutoff - ? { userId, measuredAt: { gte: sinceCutoff } } - : { userId }, + ? { userId, measuredAt: { gte: sinceCutoff }, deletedAt: null } + : { userId, deletedAt: null }, orderBy: { measuredAt: "asc" }, }); diff --git a/src/lib/insights/glp1-plateau.ts b/src/lib/insights/glp1-plateau.ts index e452c95f7..539ce74c5 100644 --- a/src/lib/insights/glp1-plateau.ts +++ b/src/lib/insights/glp1-plateau.ts @@ -74,6 +74,7 @@ export async function detectGlp1Plateau( userId, type: "WEIGHT", measuredAt: { gte: windowStart }, + deletedAt: null, }, orderBy: { measuredAt: "asc" }, select: { value: true, measuredAt: true }, diff --git a/src/lib/insights/pulse-status.ts b/src/lib/insights/pulse-status.ts index a5a5887ac..5dcb595a9 100644 --- a/src/lib/insights/pulse-status.ts +++ b/src/lib/insights/pulse-status.ts @@ -158,6 +158,7 @@ export async function generatePulseStatusForUser( where: { userId, type: "PULSE", + deletedAt: null, }, orderBy: { measuredAt: "desc" }, take: 365,