diff --git a/scripts/seed-demo.ts b/scripts/seed-demo.ts index 44d4db708..7bc70996b 100644 --- a/scripts/seed-demo.ts +++ b/scripts/seed-demo.ts @@ -13,6 +13,7 @@ */ import pg from "pg"; +import { VALUE_RANGES } from "../src/lib/validations/measurement"; const DATABASE_URL = process.env.DATABASE_URL; if (!DATABASE_URL) { @@ -55,13 +56,20 @@ function randomWalk( target: number, days: number, volatility: number, + // Optional physiological band. A free walk can drift below the floor and + // mint an out-of-range row (a systolic-0 BP reading was observed in a + // fresh seed — iOS #33); clamping every step keeps the series inside the + // same band the input validator enforces. + clamp?: { min: number; max: number }, ): number[] { const values: number[] = [start]; for (let i = 1; i < days; i++) { const prev = values[i - 1]; const drift = (target - prev) * 0.03; // mean reversion const noise = (Math.random() - 0.5) * volatility; - values.push(Math.round((prev + drift + noise) * 10) / 10); + let next = Math.round((prev + drift + noise) * 10) / 10; + if (clamp) next = Math.min(clamp.max, Math.max(clamp.min, next)); + values.push(next); } return values; } @@ -124,10 +132,10 @@ async function seed() { // Weight: 86.5 → trending down to ~82.5 const weights = randomWalk(86.5, 82.0, days, 0.6); - // Systolic BP: 132 → improving to ~124 - const sysBP = randomWalk(132, 124, days, 4); - // Diastolic BP: 85 → improving to ~78 - const diaBP = randomWalk(85, 78, days, 3); + // Systolic BP: 132 → improving to ~124 (clamped to the physiological band) + const sysBP = randomWalk(132, 124, days, 4, VALUE_RANGES.BLOOD_PRESSURE_SYS); + // Diastolic BP: 85 → improving to ~78 (clamped to the physiological band) + const diaBP = randomWalk(85, 78, days, 3, VALUE_RANGES.BLOOD_PRESSURE_DIA); // Pulse: 72 → slight improvement to ~68 const pulse = randomWalk(72, 68, days, 3); // Body fat: 24.5% → trending to ~22% diff --git a/src/app/api/measurements/series/route.ts b/src/app/api/measurements/series/route.ts index 916f9a82e..e782360e0 100644 --- a/src/app/api/measurements/series/route.ts +++ b/src/app/api/measurements/series/route.ts @@ -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 }, diff --git a/src/lib/validations/measurement.ts b/src/lib/validations/measurement.ts index 1e39e7cd5..ce3dc4574 100644 --- a/src/lib/validations/measurement.ts +++ b/src/lib/validations/measurement.ts @@ -343,7 +343,7 @@ export function getUnitForType(type: string): string { } // Plausible ranges per measurement type -const VALUE_RANGES: Record = { +export const VALUE_RANGES: Record = { WEIGHT: { min: 1, max: 500 }, BLOOD_PRESSURE_SYS: { min: 40, max: 300 }, BLOOD_PRESSURE_DIA: { min: 20, max: 200 },