mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +00:00
fix(series): follow the source priority ladder in the blood-pressure series
The blood-pressure branch of /api/measurements/series still read systolic and diastolic with an unranked findMany, so with a second provider the pairs blended both sources and a reading present in both showed twice, while the other kinds already follow the ladder since the dense and raw reads were moved onto the shared helper. Each of the two types now reads the ladder-winning source per day through the same helper. The physiological floors (systolic 40, diastolic 20) and the five-minute pairing are unchanged.
This commit is contained in:
1 parent
808b2672cd
commit
12e202958f
2 files changed
+87
-23
No files matched your search
@@ -329,29 +329,39 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
// enforced — a seed-era systolic-0, iOS #33 — must never surface as the
|
||||
// latest reading. Floors mirror VALUE_RANGES (the input validator's min);
|
||||
// this is a read-side selection guard, not a tightening of the write floor.
|
||||
// Each of the two types reads the ladder-winning source per day (see
|
||||
// `seriesRowsFrom`), like the other kinds, so a second provider is not
|
||||
// blended into the pairs and a reading present in both appears once.
|
||||
const bpPriority = await loadUserSourcePriority(user.id);
|
||||
const readBp = (
|
||||
type: "BLOOD_PRESSURE_SYS" | "BLOOD_PRESSURE_DIA",
|
||||
floor: number,
|
||||
) =>
|
||||
prisma
|
||||
.$queryRawUnsafe<
|
||||
Array<{ id: string; value: number; measured_at: Date }>
|
||||
>(
|
||||
`
|
||||
SELECT m."id", m."value", m."measured_at"
|
||||
FROM ${seriesRowsFrom(bpPriority, type, days)}
|
||||
WHERE m."measured_at" >= $2
|
||||
AND m."value" >= $3
|
||||
ORDER BY m."measured_at" ASC, m."id" ASC
|
||||
`,
|
||||
user.id,
|
||||
since,
|
||||
floor,
|
||||
)
|
||||
.then((rows) =>
|
||||
rows.map((r) => ({
|
||||
id: r.id,
|
||||
value: r.value,
|
||||
measuredAt: r.measured_at,
|
||||
})),
|
||||
);
|
||||
const [sys, dia] = await Promise.all([
|
||||
prisma.measurement.findMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
type: "BLOOD_PRESSURE_SYS",
|
||||
measuredAt: { gte: since },
|
||||
deletedAt: null,
|
||||
value: { gte: VALUE_RANGES.BLOOD_PRESSURE_SYS.min },
|
||||
},
|
||||
orderBy: { measuredAt: "asc" },
|
||||
select: { id: true, value: true, measuredAt: true },
|
||||
}),
|
||||
prisma.measurement.findMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
type: "BLOOD_PRESSURE_DIA",
|
||||
measuredAt: { gte: since },
|
||||
deletedAt: null,
|
||||
value: { gte: VALUE_RANGES.BLOOD_PRESSURE_DIA.min },
|
||||
},
|
||||
orderBy: { measuredAt: "asc" },
|
||||
select: { id: true, value: true, measuredAt: true },
|
||||
}),
|
||||
readBp("BLOOD_PRESSURE_SYS", VALUE_RANGES.BLOOD_PRESSURE_SYS.min),
|
||||
readBp("BLOOD_PRESSURE_DIA", VALUE_RANGES.BLOOD_PRESSURE_DIA.min),
|
||||
]);
|
||||
|
||||
// Pair by closest timestamp within ±5 minutes. Both reads arrive
|
||||
|
||||
@@ -114,7 +114,12 @@ async function series(userId: string, kind: string, days: number) {
|
||||
return (
|
||||
(await res.json()) as {
|
||||
data: {
|
||||
points: Array<{ id: string; at: string; value: number }>;
|
||||
points: Array<{
|
||||
id: string;
|
||||
at: string;
|
||||
value: number;
|
||||
secondary: number | null;
|
||||
}>;
|
||||
stats: { count: number; mean: number; min: number; max: number };
|
||||
};
|
||||
}
|
||||
@@ -146,6 +151,25 @@ beforeAll(async () => {
|
||||
dual.push(row(DUAL, "WEIGHT", "kg", "MANUAL", 80 + d, at(d)));
|
||||
dual.push(row(DUAL, "WEIGHT", "kg", "WITHINGS", 80 + d, at(d)));
|
||||
}
|
||||
// Blood pressure: Apple Health (120/80, 122/82) on days 1-2, Withings
|
||||
// (140/90, 142/92) on days 2-3. Day 2 has both sources; day 1 only Apple,
|
||||
// day 3 only Withings. A sub-floor systolic (0) on day 1 must stay out.
|
||||
const bp = (
|
||||
source: MeasurementSource,
|
||||
d: number,
|
||||
sys: number,
|
||||
dia: number,
|
||||
) => {
|
||||
dual.push(row(DUAL, "BLOOD_PRESSURE_SYS", "mmHg", source, sys, at(d, 10)));
|
||||
dual.push(row(DUAL, "BLOOD_PRESSURE_DIA", "mmHg", source, dia, at(d, 10)));
|
||||
};
|
||||
bp("APPLE_HEALTH", 1, 120, 80);
|
||||
bp("APPLE_HEALTH", 2, 122, 82);
|
||||
bp("WITHINGS", 2, 142, 92);
|
||||
bp("WITHINGS", 3, 140, 90);
|
||||
dual.push(
|
||||
row(DUAL, "BLOOD_PRESSURE_SYS", "mmHg", "APPLE_HEALTH", 0, at(1, 20)),
|
||||
);
|
||||
// Glucose has no ladder: both sources must stay.
|
||||
for (const d of [1, 2]) {
|
||||
dual.push(row(DUAL, "BLOOD_GLUCOSE", "mg/dL", "APPLE_HEALTH", 100, at(d)));
|
||||
@@ -220,6 +244,36 @@ describe("pulse follows the ladder (raw read, 30 days)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("blood pressure follows the ladder", () => {
|
||||
const bpSeries = async () => (await series(DUAL, "bloodPressure", 30)).points;
|
||||
|
||||
it("shows the ladder's source on a day both cover, and the other where only it exists", async () => {
|
||||
await setLadder(DUAL, { bloodPressure: ["APPLE_HEALTH", "WITHINGS"] });
|
||||
const points = await bpSeries();
|
||||
// Day 1 Apple, day 2 Apple (Withings' 142/92 is out), day 3 only Withings.
|
||||
expect(points.map((p) => [p.value, p.secondary])).toEqual([
|
||||
[140, 90],
|
||||
[122, 82],
|
||||
[120, 80],
|
||||
]);
|
||||
});
|
||||
|
||||
it("flips with the ladder", async () => {
|
||||
await setLadder(DUAL, { bloodPressure: ["WITHINGS", "APPLE_HEALTH"] });
|
||||
const points = await bpSeries();
|
||||
expect(points.map((p) => [p.value, p.secondary])).toEqual([
|
||||
[140, 90],
|
||||
[142, 92],
|
||||
[120, 80],
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps a non-physiological systolic out of the series", async () => {
|
||||
await setLadder(DUAL, { bloodPressure: ["APPLE_HEALTH", "WITHINGS"] });
|
||||
expect((await bpSeries()).every((p) => p.value >= 40)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("a reading mirrored into two sources", () => {
|
||||
it("appears once", async () => {
|
||||
await setLadder(DUAL, { weight: ["WITHINGS", "MANUAL"] });
|
||||
|
||||
Reference in new issue
Block a user