Merge remote-tracking branch 'origin/main' into fix/v1397-derive

This commit is contained in:
Marc-André Bombeck committed 2026-10-01 10:34:17 +02:00
commit da8cc7a6ee
74 files changed
+2304 -292

No files matched your search

+2 -1
View File
@@ -34,7 +34,7 @@ import {
GLUCOSE_PANEL_WINDOW_DAYS,
type GlucoseClinicalMetrics,
} from "@/lib/analytics/glucose-metrics";
import { groupByGlucoseContext } from "@/lib/glucose";
import { groupByGlucoseContext, resolveGlucoseUnit } from "@/lib/glucose";
import {
decisionForEvidence,
PATTERN_FAMILIES,
@@ -619,6 +619,7 @@ async function buildAnalyticsResponse(user: AuthedUser, locale: Locale) {
timezone: userTz,
sourcePriorityJson: scoreSourcePriority,
thresholdsJson: user.thresholdsJson ?? null,
glucoseUnit: resolveGlucoseUnit(user.glucoseUnit),
},
modules: {
glucose: glucoseEnabled,
@@ -23,6 +23,10 @@ vi.mock("@/lib/db", () => ({
vi.mock("@/lib/auth/session", () => ({ getSession: vi.fn() }));
vi.mock("@/lib/insights/unit-change-refresh", () => ({
refreshTextsAfterUnitChange: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("@/lib/auth/audit", () => ({
auditLog: vi.fn().mockResolvedValue(undefined),
}));
@@ -56,6 +60,7 @@ import { GET, PATCH } from "../route";
import { prisma } from "@/lib/db";
import { getSession } from "@/lib/auth/session";
import { auditLog } from "@/lib/auth/audit";
import { refreshTextsAfterUnitChange } from "@/lib/insights/unit-change-refresh";
import { checkRateLimit } from "@/lib/rate-limit";
const SESSION_OK = {
@@ -210,3 +215,33 @@ describe("PATCH /api/auth/me/glucose-unit", () => {
expect(prisma.user.update).not.toHaveBeenCalled();
});
});
describe("PATCH re-warms the texts that quote the unit", () => {
it("re-warms the stored notes when the unit actually changes", async () => {
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
vi.mocked(prisma.user.findUnique).mockResolvedValue({
glucoseUnit: null,
} as never);
vi.mocked(prisma.user.update).mockResolvedValue({} as never);
const res = await (PATCH as (r: Request) => Promise<Response>)(
mkPatch({ glucoseUnit: "mmol/L" }),
);
expect(res.status).toBe(200);
expect(refreshTextsAfterUnitChange).toHaveBeenCalledWith("user-1");
});
it("leaves them alone when the same unit is saved again", async () => {
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
vi.mocked(prisma.user.findUnique).mockResolvedValue({
glucoseUnit: "mmol/L",
} as never);
vi.mocked(prisma.user.update).mockResolvedValue({} as never);
const res = await (PATCH as (r: Request) => Promise<Response>)(
mkPatch({ glucoseUnit: "mmol/L" }),
);
expect(res.status).toBe(200);
expect(refreshTextsAfterUnitChange).not.toHaveBeenCalled();
});
});
@@ -38,6 +38,7 @@ import { prisma } from "@/lib/db";
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";
export const dynamic = "force-dynamic";
@@ -112,6 +113,12 @@ 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.
if (resolveGlucoseUnit(previous?.glucoseUnit) !== next) {
void refreshTextsAfterUnitChange(user.id);
}
annotate({
action: { name: "auth.me.glucose-unit.patch" },
meta: { glucoseUnit: next },
@@ -11,6 +11,10 @@ vi.mock("@/lib/db", () => ({
vi.mock("@/lib/auth/session", () => ({ getSession: vi.fn() }));
vi.mock("@/lib/insights/unit-change-refresh", () => ({
refreshTextsAfterUnitChange: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("@/lib/auth/audit", () => ({
auditLog: vi.fn().mockResolvedValue(undefined),
}));
@@ -43,6 +47,7 @@ import { GET, PATCH } from "../route";
import { prisma } from "@/lib/db";
import { getSession } from "@/lib/auth/session";
import { auditLog } from "@/lib/auth/audit";
import { refreshTextsAfterUnitChange } from "@/lib/insights/unit-change-refresh";
const SESSION_OK = {
session: { id: "sess-1", expiresAt: new Date(Date.now() + 3_600_000) },
@@ -197,3 +202,33 @@ describe("PATCH /api/auth/me/unit-preference", () => {
expect(prisma.user.update).not.toHaveBeenCalled();
});
});
describe("PATCH re-warms the texts that quote the unit", () => {
it("re-warms the stored notes when the unit actually changes", async () => {
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
vi.mocked(prisma.user.findUnique).mockResolvedValue({
unitPreference: null,
} as never);
vi.mocked(prisma.user.update).mockResolvedValue({} as never);
const res = await (PATCH as (r: Request) => Promise<Response>)(
mkPatch({ unitPreference: "imperial" }),
);
expect(res.status).toBe(200);
expect(refreshTextsAfterUnitChange).toHaveBeenCalledWith("user-1");
});
it("leaves them alone when the same unit is saved again", async () => {
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
vi.mocked(prisma.user.findUnique).mockResolvedValue({
unitPreference: "imperial",
} as never);
vi.mocked(prisma.user.update).mockResolvedValue({} as never);
const res = await (PATCH as (r: Request) => Promise<Response>)(
mkPatch({ unitPreference: "imperial" }),
);
expect(res.status).toBe(200);
expect(refreshTextsAfterUnitChange).not.toHaveBeenCalled();
});
});
@@ -38,6 +38,7 @@ import { prisma } from "@/lib/db";
import { checkRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
import { DEFAULT_UNIT_PREFERENCE } from "@/lib/measurements/display-transform";
import { unitPreferencePatchSchema } from "@/lib/validations/user-prefs";
import { refreshTextsAfterUnitChange } from "@/lib/insights/unit-change-refresh";
export const dynamic = "force-dynamic";
@@ -118,6 +119,12 @@ export const PATCH = apiHandler(async (req: Request) => {
},
});
// The status notes and narratives quote mass, length and temperature in
// this branch; re-warm them so today's stored text follows the switch.
if (resolve(previous?.unitPreference) !== next) {
void refreshTextsAfterUnitChange(user.id);
}
annotate({
action: { name: "auth.me.unit-preference.patch" },
meta: { unitPreference: next },
@@ -41,6 +41,7 @@ import {
parseNotificationPrefs,
resolveNotificationPrefs,
} from "@/lib/validations/notification-prefs";
import { refreshTextsAfterUnitChange } from "@/lib/insights/unit-change-refresh";
export const dynamic = "force-dynamic";
@@ -230,6 +231,9 @@ export const PATCH = apiHandler(
},
});
invalidateUserProfile(access.recordId);
if (patch.unitPreference !== undefined) {
void refreshTextsAfterUnitChange(access.recordId);
}
if (patch.timezone !== undefined) {
invalidateUserMedications(access.recordId, { evict: true });
}