fix(bp): keep non-physiological readings out of the seed and the series

The demo seed's random-walk BP generator could drift below the
physiological floor and mint a systolic-0 row (iOS #33). Clamp the walk
to the same band the input validator enforces (systolic >= 40,
diastolic >= 20), reusing the VALUE_RANGES floors, so a fresh seed never
writes a sub-floor reading. Add a read-side selection guard on the blood
pressure series endpoint that excludes rows below those floors from both
the series and the implicit latest point, so a corrupt row that slipped
in before the floor was enforced can never surface as the latest
reading. Export VALUE_RANGES so both sites share one source of truth.
This commit is contained in:
Marc-André Bombeck committed 2026-06-19 20:34:20 +02:00
1 parent a3b274171c
commit f002a9d565
3 files changed
+23 -6

No files matched your search

+9
View File
@@ -23,6 +23,7 @@ import { summarize, type DataPoint } from "@/lib/analytics/trends";
import { redactSensitiveFields } from "@/lib/observability/redact-payload";
import type { MeasurementType, SleepStage } from "@/generated/prisma/client";
import { reconstructSleepNights } from "@/lib/analytics/sleep-night";
import { VALUE_RANGES } from "@/lib/validations/measurement";
import { loadUserSourcePriority } from "@/lib/rollups/measurement-read";
import { resolveUserTimezone } from "@/lib/tz/resolver";
import { convertGlucose, resolveGlucoseUnit } from "@/lib/glucose";
@@ -264,6 +265,12 @@ export const GET = apiHandler(async (request: NextRequest) => {
};
});
} else if (kind === "bloodPressure") {
// Exclude non-physiological rows (systolic < 40 / diastolic < 20) from the
// series AND the implicit "latest" (the last point). BP is two rows paired
// client-side, so a corrupt row that slipped in before the input floor was
// 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.
const [sys, dia] = await Promise.all([
prisma.measurement.findMany({
where: {
@@ -271,6 +278,7 @@ export const GET = apiHandler(async (request: NextRequest) => {
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 },
@@ -281,6 +289,7 @@ export const GET = apiHandler(async (request: NextRequest) => {
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 },
+1 -1
View File
@@ -343,7 +343,7 @@ export function getUnitForType(type: string): string {
}
// Plausible ranges per measurement type
const VALUE_RANGES: Record<string, { min: number; max: number }> = {
export const VALUE_RANGES: Record<string, { min: number; max: number }> = {
WEIGHT: { min: 1, max: 500 },
BLOOD_PRESSURE_SYS: { min: 40, max: 300 },
BLOOD_PRESSURE_DIA: { min: 20, max: 200 },