fix(measurements): hour-bucket a CGM glucose series inside the 90-day window

A sensor reading every five minutes puts about 26 000 glucose readings in
a 90-day window, and the per-kind series sent every one of them raw. The
hourly bucketing that already applies to a dense heart-rate stream now
applies to glucose too, with the same 10 000-reading cap: the hour's mean
converted once to the user's unit, statistics over every raw reading.
A fingerstick meter stays far under the cap and keeps its raw points.

The doctor report keeps glucose raw inside 90 days. Its clinical panel is
computed from the raw readings of every source, while the day path works
from canonical-source day buckets, so the two do not give the same figures
when a meter and a sensor overlap.
This commit is contained in:
Marc-André Bombeck committed 2026-09-30 23:01:57 +02:00
1 parent ca10f2e5df
commit 731e073284
5 files changed
+340 -18

No files matched your search

+2 -2
View File
@@ -696,7 +696,7 @@ const seriesPointSchema = z.object({
id: z
.string()
.describe(
"Measurement row id; `sleep:<wake-day>` for a sleep night; `day:<local-day>` for a day bucket (`pulse` / `glucose` beyond 90 days); `hour:<ISO start>` for an hour bucket (`pulse` within 90 days when the window holds more than 10 000 readings).",
"Measurement row id; `sleep:<wake-day>` for a sleep night; `day:<local-day>` for a day bucket (`pulse` / `glucose` beyond 90 days); `hour:<ISO start>` for an hour bucket (`pulse` / `glucose` within 90 days when the window holds more than 10 000 readings).",
),
at: z.iso.datetime({ offset: true }).describe("Point timestamp (ISO-8601)."),
value: z.number().describe("Primary value in the top-level `unit`."),
@@ -751,7 +751,7 @@ const seriesResponse = z
.meta({
id: "MeasurementsSeriesResponse",
description:
"iOS-friendly per-kind time series: one point per reading (or per reconstructed night for `sleep`), an explicit `unit` token, and a summary `stats` block. Sample-dense kinds are bucketed server-side: `pulse` and `glucose` beyond 90 days per local day, and `pulse` within 90 days per local hour when the window holds more than 10 000 readings (a bucket carries the mean as `value` and its range as `valueMin` / `valueMax`). `stats` is always computed over the underlying readings, never over the buckets.",
"iOS-friendly per-kind time series: one point per reading (or per reconstructed night for `sleep`), an explicit `unit` token, and a summary `stats` block. Sample-dense kinds are bucketed server-side: `pulse` and `glucose` beyond 90 days per local day, and `pulse` / `glucose` within 90 days per local hour when the window holds more than 10 000 readings (a continuous glucose sensor, a watch recording heart rate every minute; a cuff or fingerstick meter stays raw). A bucket carries the mean as `value`; a `pulse` bucket also carries its range as `valueMin` / `valueMax`, and a `glucose` bucket is converted to the user's unit like a raw reading. `stats` is always computed over the underlying readings, never over the buckets.",
});
// ── Batched daily series (v1.18.6 dashboard fetch coalescing) ────────