The chart-data query in health-chart.tsx walked the full measurement
history with a `while (true)` paginated loop on every visit. For
Apple-Health-rich accounts (pulse every minute, months of backfill)
this meant tens of thousands of rows per type per page navigation,
which is the root cause behind FB-D2's "pulse insights loads forever"
report.
Derive `from` / `to` from the active range tab (plus the
comparison-baseline shift when overlay is on), pass both to
/api/measurements, and drop the unbounded paginator. The query key
re-keys on the window so a range-tab change refetches the right
slice. staleTime 60s + gcTime 5min hold the bounded payload across
tab navigation between insights sub-pages.
New `aggregate` query param + range-aggregation helper let the GET
handler downsample server-side for ranges over 90 d (one row per
day) and over 365 d (one row per ISO week). The per-request `limit`
ceiling lifts from 500 to 5000 — still bounded, but enough headroom
for an explicit from/to window without re-introducing pagination.
iOS contract — GET /api/measurements: additive. The `from`, `to`,
`aggregate` params are all optional; callers that omit them (the
iOS client, the existing measurements page) see the unchanged
response shape and the historical 500-row default cap. The
aggregated branch is only entered when both from + to are present
AND the window is wide enough for the threshold ladder to engage.
Adds 32 HKQuantity / HKCategory identifiers to HK_QUANTITY_TYPE_DEFERRED
so the batch ingest endpoint logs a "deferred, not unknown" signal when
the iOS app passes a sample type HealthLog hasn't surfaced yet. None
currently have a HealthLog MeasurementType counterpart; mapping them
through requires the enum extension to land first (separate wave).
Buckets covered, with iOS version they shipped in:
- Cardiovascular clinical (iOS 16): AFib burden, peripheral perfusion
- Mobility (iOS 14/15): walking steadiness, falls, steadiness event
- Pulmonary clinical (iOS 17): FEV1, FVC, peak expiratory flow,
inhaler usage
- Other quantity types: insulin delivery, UV exposure, electrodermal
activity, blood-alcohol content, legacy Nike fuel
- Heart-rhythm event flags (iOS 9+/17 refresh): low/high heart-rate
events, irregular rhythm, low cardio fitness event
- Audio-exposure event flags + environmental sound reduction
- Behavioural: handwashing, toothbrushing
- Reproductive / fertility / pregnancy: contraceptive, lactation,
pregnancy, pregnancy/progesterone test, sexual activity, sleep
changes, persistent intermenstrual bleeding, prolonged / irregular
/ infrequent menstrual cycles
Tests gain explicit coverage that every long-tail identifier sits in
the deferred set and that no identifier is duplicated.
Builds the base output object once with the four shared fields and
appends sleepStage only when the mapping carries a sleep-stage table.
Drops one duplicated object literal and one return path; the function
keeps the same null-on-bad-input semantics for unmapped, invalid date,
missing stage, and unknown stage cases.
The derived map mirrored APPLE_HEALTH_TYPE_MAP one key for one key
with the value narrowed to the measurementType field. Every caller can
read that via APPLE_HEALTH_TYPE_MAP[id]?.measurementType — no external
consumer is depending on the compact view today. Drops the redundant
constant plus the mirror-check test block that only existed to keep
the two tables in sync.
Extends src/lib/measurements/apple-health-mapping.ts with the W8d
mapping surface the v1.5 iOS-Swift session needs locked before it
starts:
- Three new APPLE_HEALTH_TYPE_MAP entries —
HKQuantityTypeIdentifierEnvironmentalAudioExposure,
HeadphoneAudioExposure, TimeInDaylight — wired to the matching
MeasurementType values landed in Migration 0052.
- HK_QUANTITY_TYPE_TO_MEASUREMENT: a compact derived view of the
same data keyed by HK identifier so a caller that only needs the
enum value can skip instantiating the full mapping record (XML
import worker, iOS DTO routing).
- HK_QUANTITY_TYPE_DEFERRED: the curated "we know about it, we are
not mapping it yet" set. Covers iOS-17/18 running form, cycling,
paddle / row / ski distances + speeds, sleep apnea, scored
assessments (GAD-7 / PHQ-9), nutrition, mental-state, FHIR
clinical, ECG, pregnancy / cycle. Each block carries its planned
release window inline.
File-header attribution credits the two open-source projects whose
identifier lists were consulted as data —
k0rventen/apple-health-grafana (MIT) and dogsheep/healthkit-to-sqlite
(Apache-2.0); only the lookup strings were ported, not code.
Test additions
(src/lib/measurements/__tests__/apple-health-mapping.test.ts):
- exhaustiveness — every MeasurementType outside the
Withings-only set has an HK counterpart;
- dedup — no identifier appears in both the mapping table and the
deferred set;
- identity-conversion check for the three new entries;
- HK_QUANTITY_TYPE_TO_MEASUREMENT key + value parity with the full
map;
- deferred-set sanity (every entry starts with a real HK prefix).
Mostly table column normalisation under docs/audit/ + planning, object-
literal expansion in src/lib/ai/coach/snapshot.ts after the W4 F6 Apple
Health metric block landed, and small wrap/unwrap line adjustments in
the device + APNs routes / tests touched across W3/W4/W5 sessions.
No behavioural change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New `src/lib/measurements/apple-health-mapping.ts` translates inbound
HealthKit samples into the row shape the `Measurement` model expects.
Symmetrical to the Withings `MEASURE_TYPE_MAP` but keyed by HealthKit
string identifier so the upcoming batch endpoint can do a single
table lookup per entry.
Each mapping carries its canonical DB unit, a unit-conversion helper
(identity for SI-aligned types, ×100 for HealthKit's 0..1 fraction
quantities), an aggregation hint for downstream summarisation, and
flags the explicit-consent metrics (HRV, VO2 Max, sleep) so the audit
trail can pick them out.
Sleep is the one category type — the helper returns one row per stage
with the new `sleepStage` column populated; per-stage minutes go
straight in as `value`. Unknown identifiers return `null` so the
batch endpoint can mark the entry as skipped instead of failing the
whole batch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>