From c775861d8c75059675d4e32e924bfd5cd87d91e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 31 May 2026 22:42:05 +0200 Subject: [PATCH] fix(insights): invalidate per-metric assessment caches on measurement edit and delete The targeted status-insight invalidator only fired on insert and batch ingest, so editing or deleting a reading left the affected per-metric assessment cards serving their pre-mutation text for the rest of the day. Add the fire-and-forget invalidation to the single-measurement PUT (covering both the old and the new type when an edit re-types a row), the single-measurement DELETE, and the iOS by-external-ids reconciliation DELETE (keyed off the distinct types it tombstoned). --- src/app/api/measurements/[id]/route.ts | 20 +++++++++++++++++++ .../api/measurements/by-external-ids/route.ts | 12 +++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/app/api/measurements/[id]/route.ts b/src/app/api/measurements/[id]/route.ts index 5939d75e9..e6077de71 100644 --- a/src/app/api/measurements/[id]/route.ts +++ b/src/app/api/measurements/[id]/route.ts @@ -12,6 +12,7 @@ import { } from "@/lib/api-response"; import { updateMeasurementSchema } from "@/lib/validations/measurement"; import { invalidateUserMeasurements } from "@/lib/cache/invalidate"; +import { invalidateStatusInsightsForTypes } from "@/lib/insights/comprehensive-generate"; import { recomputeBucketsForMeasurement } from "@/lib/rollups/measurement-rollups"; import { Prisma } from "@/generated/prisma/client"; import { NextRequest } from "next/server"; @@ -172,6 +173,18 @@ export const PUT = apiHandler( console.warn("[measurements] rollup recompute failed", err); } + // v1.8.0 — drop the cached per-metric assessment rows this edit + // dirties so the next mount / nightly warm pass regenerates against + // the new value. An edit can re-type a row, so invalidate both the + // old and the new type's scopes. Fire-and-forget: never blocks the + // user's edit. + invalidateStatusInsightsForTypes(user.id, [ + existing.type, + measurement.type, + ]).catch((err) => { + console.warn("[measurements] status-insight invalidate failed", err); + }); + return apiSuccess(measurement); }, ); @@ -238,6 +251,13 @@ export const DELETE = apiHandler( console.warn("[measurements] rollup recompute failed", err); } + // v1.8.0 — drop the cached per-metric assessment rows this deletion + // dirties so the next mount / nightly warm pass regenerates against + // the reduced history. Fire-and-forget: never blocks the user's delete. + invalidateStatusInsightsForTypes(user.id, [existing.type]).catch((err) => { + console.warn("[measurements] status-insight invalidate failed", err); + }); + return apiSuccess({ deleted: true }); }, ); diff --git a/src/app/api/measurements/by-external-ids/route.ts b/src/app/api/measurements/by-external-ids/route.ts index ced87b1e4..1e25aa07e 100644 --- a/src/app/api/measurements/by-external-ids/route.ts +++ b/src/app/api/measurements/by-external-ids/route.ts @@ -35,6 +35,7 @@ import { safeJson, } from "@/lib/api-response"; import { invalidateUserMeasurements } from "@/lib/cache/invalidate"; +import { invalidateStatusInsightsForTypes } from "@/lib/insights/comprehensive-generate"; import { recomputeBucketsForMeasurement, collapseToTypeDayKeys, @@ -152,6 +153,17 @@ async function deleteByExternalIds(request: NextRequest): Promise { } catch (err) { console.warn("[measurements] rollup recompute failed", err); } + + // v1.8.0 — drop the cached per-metric assessment rows the deleted + // types dirty so the next mount / nightly warm pass regenerates + // against the reconciled history. Fire-and-forget: never blocks the + // iOS reconciliation. + invalidateStatusInsightsForTypes( + user.id, + affectedRows.map((row) => row.type), + ).catch((err) => { + console.warn("[measurements] status-insight invalidate failed", err); + }); } return apiSuccess({ deletedCount: result.count });