mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 15:02:04 +00:00
fix(insights): write metric status notes in the reader's units
The per-metric status note quoted glucose in mg/dL, body composition in kg, temperatures in °C and walking distance in metres, whatever the reader had chosen; the deterministic floor without a provider did the same. The generator now converts the series, the reference band and the signal into the reader's units before they reach the prompt or the floor, and prints the guideline band table in that unit while still classifying on the canonical value. A changed glucose unit or metric/imperial preference (own account or a managed record) now re-warms the stored notes and narratives through the hash-gated refill, so today's text does not keep the old unit. Refs #1067
This commit is contained in:
1 parent
70e6e07a89
commit
6c6d363bba
13 files changed
+551
-28
No files matched your search
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user