mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
feat(glucose): read glucose in one unit across series, CSV, and detail page
Glucose is stored canonical mg/dL but the series DTO, CSV export, and detail page emitted raw mg/dL while FHIR already converted to the user preference. Convert at serialization through the single convertGlucose helper: - series route sets unit from the user's glucoseUnit and converts each point - CSV export converts BLOOD_GLUCOSE rows and emits the unit in the unit column - the detail page + stat strip honour the preference via the chart value scale - declare the trailing-90-day window in the glucose median label across locales mmol/L-preference users now see the blood-glucose page, stat strip, iOS series, and CSV in mmol/L; mg/dL users are unchanged.
This commit is contained in:
1 parent
a2260d5bb8
commit
25944ed57a
17 files changed
+398
-26
No files matched your search
@@ -135,6 +135,66 @@ describe("GET /api/export/measurements", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
// v1.16.16 — glucose unit-at-source. A mmol/L-preference user's CSV emits
|
||||
// BLOOD_GLUCOSE converted (100 → 5.5) with `mmol/L` in the unit column.
|
||||
it("exports BLOOD_GLUCOSE in the user's mmol/L preference", async () => {
|
||||
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
|
||||
vi.mocked(checkRateLimit).mockResolvedValue({
|
||||
allowed: true,
|
||||
remaining: 9,
|
||||
resetAt: Date.now() + 3600_000,
|
||||
});
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue({
|
||||
glucoseUnit: "mmol/L",
|
||||
} as never);
|
||||
vi.mocked(prisma.measurement.findMany).mockResolvedValue([
|
||||
{
|
||||
type: "BLOOD_GLUCOSE",
|
||||
value: 100,
|
||||
unit: "mg/dL",
|
||||
measuredAt: new Date("2026-05-01T08:00:00.000Z"),
|
||||
source: "MANUAL",
|
||||
notes: null,
|
||||
glucoseContext: "FASTING",
|
||||
},
|
||||
] as never);
|
||||
|
||||
const { GET } = await import("../measurements/route");
|
||||
const res = await GET(mkReq("http://localhost/api/export/measurements"));
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.text();
|
||||
expect(body).toContain("BLOOD_GLUCOSE,5.5,mmol/L");
|
||||
expect(body).not.toContain("100,mg/dL");
|
||||
});
|
||||
|
||||
it("exports BLOOD_GLUCOSE in raw mg/dL for a mg/dL-preference user", async () => {
|
||||
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
|
||||
vi.mocked(checkRateLimit).mockResolvedValue({
|
||||
allowed: true,
|
||||
remaining: 9,
|
||||
resetAt: Date.now() + 3600_000,
|
||||
});
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue({
|
||||
glucoseUnit: "mg/dL",
|
||||
} as never);
|
||||
vi.mocked(prisma.measurement.findMany).mockResolvedValue([
|
||||
{
|
||||
type: "BLOOD_GLUCOSE",
|
||||
value: 100,
|
||||
unit: "mg/dL",
|
||||
measuredAt: new Date("2026-05-01T08:00:00.000Z"),
|
||||
source: "MANUAL",
|
||||
notes: null,
|
||||
glucoseContext: "FASTING",
|
||||
},
|
||||
] as never);
|
||||
|
||||
const { GET } = await import("../measurements/route");
|
||||
const res = await GET(mkReq("http://localhost/api/export/measurements"));
|
||||
const body = await res.text();
|
||||
expect(body).toContain("BLOOD_GLUCOSE,100,mg/dL");
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/export/medications", () => {
|
||||
|
||||
@@ -25,6 +25,7 @@ import { checkRateLimit } from "@/lib/rate-limit";
|
||||
import { toCSV, formatMeasurementsForExport } from "@/lib/export";
|
||||
import { resolveUserTimezone } from "@/lib/tz/resolver";
|
||||
import { loadUserSourcePriority } from "@/lib/rollups/measurement-read";
|
||||
import { resolveGlucoseUnit } from "@/lib/glucose";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export const GET = apiHandler(async (request: NextRequest) => {
|
||||
@@ -45,20 +46,29 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
? "raw"
|
||||
: "night";
|
||||
|
||||
const [measurements, userTz, sourcePriorityJson] = await Promise.all([
|
||||
prisma.measurement.findMany({
|
||||
where,
|
||||
orderBy: { measuredAt: "desc" },
|
||||
}),
|
||||
resolveUserTimezone(user.id),
|
||||
loadUserSourcePriority(user.id),
|
||||
]);
|
||||
const [measurements, userTz, sourcePriorityJson, profile] =
|
||||
await Promise.all([
|
||||
prisma.measurement.findMany({
|
||||
where,
|
||||
orderBy: { measuredAt: "desc" },
|
||||
}),
|
||||
resolveUserTimezone(user.id),
|
||||
loadUserSourcePriority(user.id),
|
||||
// v1.16.16 — resolve the glucose display unit so BLOOD_GLUCOSE rows
|
||||
// export in the user's unit (matching FHIR), not raw stored mg/dL.
|
||||
prisma.user.findUnique({
|
||||
where: { id: user.id },
|
||||
select: { glucoseUnit: true },
|
||||
}),
|
||||
]);
|
||||
const glucoseUnit = resolveGlucoseUnit(profile?.glucoseUnit ?? null);
|
||||
|
||||
const csv = toCSV(
|
||||
formatMeasurementsForExport(measurements, userTz, {
|
||||
granularity,
|
||||
sleepTz: userTz,
|
||||
sourcePriorityJson,
|
||||
glucoseUnit,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
Reference in new issue
Block a user