From 375d5c3ec5499a7efa8834eae6c3a36dbb798dad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 20 Jun 2026 12:51:58 +0200 Subject: [PATCH] fix(dashboard): hard-evict snapshot on background sync paths The in-app measurement routes evict the snapshot cache, but the iOS batch ingest and the server-side Withings sync only marked it stale (or never touched it), so a focus-refetch could serve pre-sync data for up to the snapshot TTL. Route both background-sync entry points through a hard evict so returning to the dashboard surfaces freshly-synced rows. --- src/app/api/measurements/batch/route.ts | 7 ++++++- src/lib/withings/sync.ts | 10 ++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/app/api/measurements/batch/route.ts b/src/app/api/measurements/batch/route.ts index 0974fab9b..4cfb40d7d 100644 --- a/src/app/api/measurements/batch/route.ts +++ b/src/app/api/measurements/batch/route.ts @@ -648,7 +648,12 @@ async function postBatch(request: NextRequest): Promise { // and must invalidate every consumer that reads through this user's // measurements. if (insertedCount > 0 || updatedCount > 0) { - invalidateUserMeasurements(user.id); + // v1.18.9 (#38) — hard-evict so a focus-refetch after a background + // iOS / Apple-Health batch sync returns post-sync data. A mark-stale + // would let the `cachedSwr` snapshot serve the pre-batch body on the + // very next read, leaving the dashboard up to ~180 s stale; the manual + // measurement routes already pass `{ evict: true }` for the same reason. + invalidateUserMeasurements(user.id, { evict: true }); // v1.5.0 — refresh the persistent rollup table for every distinct // (type, day) the batch touched. We use the `prepared` rows diff --git a/src/lib/withings/sync.ts b/src/lib/withings/sync.ts index 5679565ac..999d70d60 100644 --- a/src/lib/withings/sync.ts +++ b/src/lib/withings/sync.ts @@ -29,6 +29,7 @@ import { recomputeBucketsForMeasurement, } from "@/lib/rollups/measurement-rollups"; import { invalidateStatusInsightsForTypes } from "@/lib/insights/comprehensive-generate"; +import { invalidateUserDashboardSnapshot } from "@/lib/cache/invalidate"; /** * Build the callback URL handed to Withings at `Notify.subscribe` time. @@ -264,6 +265,15 @@ export async function syncUserMeasurements( `withings: status-insight invalidate failed for ${userId}: ${err}`, ); }); + + // v1.18.9 (#38) — hard-evict the dashboard snapshot so a focus-refetch + // after this server-side sync returns the freshly-imported readings. + // The server-side Withings path produces no client mutation event, so + // without this the snapshot's `cachedSwr` entry kept serving the + // pre-sync body until its ~180 s TTL lapsed. + if (keys.length > 0) { + invalidateUserDashboardSnapshot(userId); + } } catch (err) { getEvent()?.addWarning( `withings: rollup recompute failed for ${userId}: ${err}`,