diff --git a/src/app/api/insights/targets/route.ts b/src/app/api/insights/targets/route.ts index 33f8dd864..0489c09df 100644 --- a/src/app/api/insights/targets/route.ts +++ b/src/app/api/insights/targets/route.ts @@ -25,6 +25,8 @@ import type { } from "@/generated/prisma/client"; import { apiHandler, requireAuth } from "@/lib/api-handler"; import { annotate } from "@/lib/logging/context"; +import { cached, caches, type ServerCache } from "@/lib/cache/server-cache"; +import { NO_STORE_BUT_BFCACHE } from "@/lib/http/cache-headers"; import { getEffectiveRange, type ThresholdOverridesJson, @@ -105,9 +107,32 @@ interface TargetPageSummary { streakHighlight: { metric: string; days: number } | null; } +/** + * v1.4.36 W1 — `/api/insights/targets` body, lifted out of the route + * handler so `cached()` can wrap it. The handler attaches the + * bfcache-friendly Cache-Control header afterward. 60 s TTL keyed on + * the user id matches the analytics cache so multiple Insights mounts + * inside a minute all hit a warm cache. Invalidated alongside the + * analytics bucket on measurement / mood / medication writes. + */ +type AuthedUser = Awaited>["user"]; + export const GET = apiHandler(async () => { const { user } = await requireAuth(); + const body = await cached( + caches.insightsTargets as ServerCache< + Awaited> + >, + user.id, + () => buildTargetsResponse(user), + annotate, + ); + const response = apiSuccess(body); + response.headers.set("Cache-Control", NO_STORE_BUT_BFCACHE); + return response; +}); +async function buildTargetsResponse(user: AuthedUser) { const userId = user.id; // v1.4.25 W7b — every per-day bucket key in this route resolves @@ -1179,7 +1204,7 @@ export const GET = apiHandler(async () => { }, }); - return apiSuccess({ + return { targets, pageSummary, // Extra diastolic data for BP display @@ -1194,5 +1219,5 @@ export const GET = apiHandler(async () => { gender, glucoseUnit, }, - }); -}); + }; +} diff --git a/src/lib/cache/invalidate.ts b/src/lib/cache/invalidate.ts index 4e8f57997..93dcf1f24 100644 --- a/src/lib/cache/invalidate.ts +++ b/src/lib/cache/invalidate.ts @@ -31,6 +31,11 @@ export function invalidateUserMeasurements(userId: string): void { caches.analytics.deleteByPrefix(`${userId}|`); caches.achievements.deleteByPrefix(userId); caches.workouts.deleteByPrefix(`${userId}|`); + // v1.4.36 W1 — measurement writes change the per-target consistency + // strips, the in-range rates and the streak counters that + // `/api/insights/targets` computes. Evict the user's bucket so the + // next mount paints fresh data. + caches.insightsTargets.deleteByPrefix(userId); } /** @@ -44,6 +49,9 @@ export function invalidateUserMood(userId: string): void { caches.moodAnalytics.deleteByPrefix(userId); caches.achievements.deleteByPrefix(userId); caches.analytics.deleteByPrefix(`${userId}|`); + // v1.4.36 W1 — mood writes change the mood target rows on the + // insights/targets response (MOOD_SCORE, MOOD_STABILITY). + caches.insightsTargets.deleteByPrefix(userId); } /** @@ -56,6 +64,9 @@ export function invalidateUserMedications(userId: string): void { caches.medications.deleteByPrefix(userId); caches.medicationsIntake.deleteByPrefix(`${userId}|`); caches.achievements.deleteByPrefix(userId); + // v1.4.36 W1 — medication writes change the MEDICATION_COMPLIANCE + // target rollup (compliance7 / compliance30 / consistency strip). + caches.insightsTargets.deleteByPrefix(userId); } /** diff --git a/src/lib/cache/server-cache.ts b/src/lib/cache/server-cache.ts index 737ef9ba3..7ed377326 100644 --- a/src/lib/cache/server-cache.ts +++ b/src/lib/cache/server-cache.ts @@ -247,6 +247,19 @@ export const caches = { maxEntries: 1000, ttlMs: 60_000, }), + /** + * v1.4.36 W1 — `/api/insights/targets` is read on every Insights + * mount and runs >1 s cold (it pulls 30-day measurements for every + * type + medications + intakes + mood + glucose). 60 s TTL matches + * the analytics cache so multiple sub-page mounts inside a minute + * all hit a warm cache. Invalidated alongside the analytics bucket + * on measurement / mood / medication writes. + */ + insightsTargets: new ServerCache({ + name: "insightsTargets", + maxEntries: 1000, + ttlMs: 60_000, + }), } as const; /**