diff --git a/src/app/api/auth/me/glucose-unit/route.ts b/src/app/api/auth/me/glucose-unit/route.ts index 719d3f45a..cd315578a 100644 --- a/src/app/api/auth/me/glucose-unit/route.ts +++ b/src/app/api/auth/me/glucose-unit/route.ts @@ -34,6 +34,7 @@ import { checkRateLimit, rateLimitHeaders } from "@/lib/rate-limit"; import { resolveGlucoseUnit, type GlucoseUnit } from "@/lib/glucose"; import { glucoseUnitPatchSchema } from "@/lib/validations/user-prefs"; import { refreshTextsAfterUnitChange } from "@/lib/insights/unit-change-refresh"; +import { invalidateUserProfile } from "@/lib/cache/invalidate"; export const dynamic = "force-dynamic"; @@ -103,9 +104,11 @@ export const PATCH = apiHandler(async (req: Request) => { }, }); - // The status notes and narratives quote glucose in this unit; re-warm - // them so today's stored text does not keep the old one. + // The targets payload and the status notes and narratives carry glucose in + // this unit: evict the cached payloads and re-warm today's stored text so + // neither keeps the old one. if (resolveGlucoseUnit(previous?.glucoseUnit) !== next) { + invalidateUserProfile(user.id); void refreshTextsAfterUnitChange(user.id); } diff --git a/src/components/settings/__tests__/glucose-unit-select.test.tsx b/src/components/settings/__tests__/glucose-unit-select.test.tsx index 8893020e8..49584a291 100644 --- a/src/components/settings/__tests__/glucose-unit-select.test.tsx +++ b/src/components/settings/__tests__/glucose-unit-select.test.tsx @@ -134,6 +134,19 @@ describe("Settings — GlucoseUnitSelect", () => { expect(componentSource).toMatch(/queryKey: queryKeys\.authMe\(\)/); }); + // The targets panel takes its unit from the targets payload, not from the + // account, so a save that only refreshed `/me` left it on the old unit + // until the server cache turned over (up to ten minutes). Both ends have + // to let go of the old payload. + it("refreshes the targets payload on both ends", () => { + expect(componentSource).toMatch(/queryKey: queryKeys\.insightsTargets\(\)/); + const route = readFileSync( + join(process.cwd(), "src/app/api/auth/me/glucose-unit/route.ts"), + "utf8", + ); + expect(route).toMatch(/!== next\) \{\s*invalidateUserProfile\(user\.id\);/); + }); + it("is actually mounted on the settings page", () => { // A control nobody can reach is the same defect as no control. const accountSection = readFileSync( diff --git a/src/components/settings/glucose-unit-select.tsx b/src/components/settings/glucose-unit-select.tsx index 234ec0434..e98274c0b 100644 --- a/src/components/settings/glucose-unit-select.tsx +++ b/src/components/settings/glucose-unit-select.tsx @@ -31,7 +31,8 @@ import { resolveGlucoseUnit, type GlucoseUnit } from "@/lib/glucose"; * Persistence mirrors the unit-system select: a change PATCHes * `/api/auth/me/glucose-unit` immediately and invalidates `queryKeys.authMe()`, * so the dashboard tiles and the glucose insight page re-render on the next - * /me refetch. + * /me refetch, and `queryKeys.insightsTargets()`, whose payload states the + * target ranges in the unit it was built with. */ export function GlucoseUnitSelect({ isAuthenticated, @@ -66,6 +67,7 @@ export function GlucoseUnitSelect({ onSuccess: () => { toastWrittenOutcome("success", t("settings.dashboard.glucoseUnit.saved")); queryClient.invalidateQueries({ queryKey: queryKeys.authMe() }); + queryClient.invalidateQueries({ queryKey: queryKeys.insightsTargets() }); setOptimistic(null); }, onError: (err) => { diff --git a/src/lib/cache/invalidate.ts b/src/lib/cache/invalidate.ts index 0081f1eea..4df20d5a0 100644 --- a/src/lib/cache/invalidate.ts +++ b/src/lib/cache/invalidate.ts @@ -242,10 +242,11 @@ export function invalidateUserHealthScore(userId: string): void { * on the very next read. * * Callers: `/api/user/thresholds` PUT + DELETE, `applyProfileUpdate` - * (only when heightCm / dateOfBirth / gender is in the update set), and - * `/api/auth/me/source-priority` PUT. The unit-preference toggle is NOT - * wired in: it only drives the client-side display transform, no - * server-cached payload reads it. + * (only when heightCm / dateOfBirth / gender is in the update set), + * `/api/auth/me/source-priority` PUT, and `/api/auth/me/glucose-unit` PATCH + * (the targets payload states glucose in the account's unit). The + * unit-preference toggle is NOT wired in: it only drives the client-side + * display transform, no server-cached payload reads it. */ export function invalidateUserProfile(userId: string): void { caches.insightsTargets.deleteByPrefix(userId);