Two additive measurement_type values for the Oura coverage completion:
- SLEEP_SCORE — the headline 0-100 sleep-quality score (unit score),
kept apart from the WHOOP sleep sub-scores so two vendors' headline
numbers never share a bucket.
- BODY_TEMPERATURE_DEVIATION — a signed nightly body-temperature offset
from the personal baseline (celsius). It is a deviation, not an
absolute reading, so it cannot share the body / skin / wrist
temperature buckets.
Migration 0165 extends the enum (additive, no backfill). Register both in
the validation enum, units, plausibility ranges, category overlay, list-UI
label/icon/colour maps, chart-token allowlist, and the PR-direction switch
(both resolve to no record direction). Both are descriptive signals, not
clinical vitals, so they stay out of the doctor-report PDF. Localise the
two list labels across all six locales and regenerate the OpenAPI contract.
Polar Nightly Recharge and Training Load Pro carry two native signals with
no existing home: the HRV-based autonomic-nervous-system charge (distinct
from the 1-6 recovery band that maps to RECOVERY_SCORE) and the device-native
cardio-load strain figure (distinct from WHOOP DAY_STRAIN and the COMPUTED
STRAIN_SCORE). Add both to the canonical enum (migration 0166) and wire them
through the validation enum, canonical units, plausibility ranges, category
cluster, chart-token allowlist, PR-direction resolver, and the measurement-
list label/icon/colour maps, with labels across all six locales. Both are
device-derived composites, so they read no PR direction and stay out of the
clinical vitals PDF, matching the WHOOP-native score classes.
Regenerate the OpenAPI contract for the two new enum members.
Add three MeasurementType values for WHOOP fields a coverage audit found
uncaptured: AVERAGE_HEART_RATE and MAX_HEART_RATE (the cycle score's
whole-day aggregates, kept distinct from spot PULSE / RESTING_HEART_RATE /
WALKING_HEART_RATE_AVERAGE) and SLEEP_DISTURBANCE_COUNT (the per-night
disturbance tally). Units: bpm / bpm / count.
Wiring mirrors the existing per-type surfaces, traced from VO2_MAX and the
v1.11.0 WHOOP-native score classes:
- canonical enum + unit + plausibility range in
src/lib/validations/measurement.ts (every ingest / analytics / rollup
path derives from measurementTypeEnum.options, so the mean-based rollup
tier picks the new types up automatically).
- Prisma enum + migration 0125 (ALTER TYPE ADD VALUE IF NOT EXISTS,
non-transactional, idempotent — same pattern as 0111).
- category overlay (cardiovascular for the HR pair, sleep for the count).
- list-view label key / Lucide icon / chart-colour.
- i18n label key in all six locales.
- chart-token allowlist + orphan-enum stripper.
- PersonalRecord direction (HR pair null — no clean goal axis, like
WALKING_HEART_RATE_AVERAGE; disturbance count MIN, like
BREATHING_DISTURBANCES).
- enum-coverage drift guard (count 60 -> 63, doctor-PDF exclusions for the
device-derived signals) + apple-health HK-counterpart exclusion (these
are WHOOP-only; HealthKit ships no first-class daily-aggregate HR
quantity nor a disturbance tally).
The types render on the measurement list + Insights chart surfaces; like the
v1.11.0 WHOOP-native scores they get no dedicated Insights sub-page (the
sub-page map stays curated) and no LOINC / FHIR mapping (not clinical-grade).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the WHOOP integration data layer (migration 0111): seven new
MeasurementType values (HRV_RMSSD, DAY_STRAIN, WORKOUT_STRAIN,
SLEEP_PERFORMANCE, SLEEP_EFFICIENCY, SLEEP_CONSISTENCY, SLEEP_NEED,
ENERGY_EXPENDITURE_KJ), a WHOOP MeasurementSource, the WhoopConnection
and WhoopOAuthState models, and the per-user encrypted BYO-key columns
on User. Day-strain takes DAY_STRAIN (not the COMPUTED STRAIN_SCORE) and
RMSSD takes HRV_RMSSD (not the SDNN HEART_RATE_VARIABILITY) so a native
value never shares a bucket with a derived proxy.
Carry the new types through the completeness walls (units, value ranges,
chart tokens, PR direction, categories, list-meta colours/icons/labels,
HealthKit-mapping exclusions) and feed the cross-source picker: recovery
ladder (WHOOP native above the COMPUTED proxy), WHOOP inserted into the
overlapping vital ladders and the workout ladder, plus skinTemperature
and respiratoryRate metric keys.
Add a server-owned COMPUTED measurement source for rows a server engine
derives rather than a device or a person reports, plus three first-class
0-100 score types — RECOVERY_SCORE, STRESS_SCORE, STRAIN_SCORE (unit
"score", plausibility range 0..100).
COMPUTED is read-only from a client's perspective: it joins the read /
decode enum so the native client can decode the rows it queries and
charts, but every client-facing write surface rejects it exactly like
WITHINGS and IMPORT — the batch ingest allowlist stays {APPLE_HEALTH,
MANUAL} and the single-entry POST 422s a COMPUTED source.
All three score types thread through every type-exhaustiveness site: the
unit + plausibility-range maps, the PR-direction resolver (scores carry
no record direction, like the device events), the category overlay (a
new "scores" cluster), the list-view label / icon / colour maps with
locale strings across all six bundles, and the insight chart-token
allowlist + orphan-enum stripper. They are not clinical vitals, so they
stay out of the doctor-report PDF.
Only the recovery score is computed today; the stress and strain types
are defined now so the engines that compute them later need no schema
change. Migration 0106 is purely additive (ADD VALUE IF NOT EXISTS).
Add a discrete EVENT row class to the Measurement model for the
device-flagged health notifications a wearable produces on its own —
irregular-rhythm, high/low heart rate, walking-steadiness and
breathing-disturbance alerts.
Following the sleep-stage / glucose-context nullable-context precedent,
this is an additive nullable column on the existing table, not a new
storage paradigm. Five new MeasurementType values carry the event class
and a new rhythm_classification enum column carries the device's own
verdict / severity (migration 0105, with a CHECK that pins the column to
the event types). An event row stores value 1 and the device verdict;
it never participates in trend, rollup, chart or personal-record
analytics.
For the irregular-rhythm class, HealthLog ingests only the
classification RESULT the device's own certified on-device algorithm
already produced. It never ingests a raw ECG waveform, never
re-classifies, and never derives a verdict of its own.
The Apple Health batch ingest maps the five category identifiers, with
the device's HKCategoryValue codepoint resolved to the stored
classification; an unknown codepoint degrades to a fallback verdict
rather than dropping the event.
Wire seven previously-deferred HealthKit quantity identifiers through
the full pipeline — enum, mapping, plausibility range, category, list-UI
metadata, FHIR coding, and the generic metric-status assessment:
CARDIO_RECOVERY heartRateRecoveryOneMinute bpm
WRIST_TEMPERATURE appleSleepingWristTemperature celsius
FALL_COUNT numberOfTimesFallen count
SIX_MINUTE_WALK_DISTANCE sixMinuteWalkTestDistance m
STAIR_ASCENT_SPEED stairAscentSpeed m/s
STAIR_DESCENT_SPEED stairDescentSpeed m/s
BREATHING_DISTURBANCES appleSleepingBreathingDisturbances count
Each value rides raw on the wire (identity conversion). Fall count is
cumulative (daily SUM); the stair speeds drain to a daily mean; the rest
store as latest per-workout / per-night spot readings. Wrist temperature
and breathing disturbances are flagged privacy-sensitive. The category
sleep-apnea and walking-steadiness EVENT identifiers stay deferred — they
are events, not quantities.
Each new metric gains an Insights sub-page on the shared scaffold with the
generic assessment card, localised across all six locales. Cardio fitness
(VO2 max) — ingested since v1.4.23 but assessment-less — gains its own
/insights/cardio-fitness page so it picks up the assessment instead of
riding as a bare chart-row on the pulse page.
Normal-range anchors are cited where defensible (six-minute walk 400-700 m,
ATS guideline + population reference); the rest defer to the user's own
baseline rather than fabricate a band.
Migration 0104 appends the enum values additively (IF NOT EXISTS, forward-only).
The iOS team had gated both client-side waiting on the server map.
Both flow raw SI on the wire (metres and metres per second
respectively) — the server-side ×100 scaling the percent gait
metrics use does NOT apply here. The convention block at the top of
apple-health-mapping.ts now spells the split out so a future
gait-metric addition picks the right path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds RESPIRATORY_RATE, BODY_MASS_INDEX, and the four remaining
deferred quantity types to MeasurementType plus the Apple Health
mapping. The batch route already returns 200 with a per-entry skip
for unmappable identifiers; iOS reads that as success and advances
its sync anchor, so any sample with one of these identifiers used
to be dropped forever. The mapping now accepts them and the rows
land.
The walking-asymmetry and walking-double-support percent values
adopt the project's server-side scaling convention (raw HK reading
on the wire, server applies the ×100 at ingest), matching the
walking-steadiness path. iOS releases that still send the
pre-multiplied percent need a one-release shim and are tracked in
the iOS coord notes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three discovery follow-ups from prior server reviews:
- REG-9: pr-detection-worker now aggregates cumulative HK kinds (steps, active energy, flights climbed, walking-running distance, time in daylight) by date_trunc('day', measured_at) SUM before picking the best day, instead of picking the largest single per-hour slice.
- New intake-auto-skip cron at :05 hourly flips MedicationIntakeEvent.skipped=true for events where skipped=false AND takenAt IS NULL AND scheduledFor < NOW() - INTERVAL '24 hours', so the compliance tile stops seeing forgotten doses as pending forever.
- /api/admin/notifications/test now handles the APNS channel (was falling through to "Unknown channel type"); surfaces the APNs sender's reason on failure.
5076 -> 5089 tests (+13). 4 pre-existing date-rollover flakes in dashboard/summary, medications/intake, and medication-card-symmetry tests are unchanged by this PR (verified against pre-fix baseline f18654f3).
Six touch-disjoint waves landed on develop before this release:
- W1-KNIP — exports/types tiers flipped to enforcing-mode on main;
35 unused exports + 52 unused types triaged to 0/0; shadcn surface
+ zod-infer contract types scoped via knip.json ignore-block.
- W2-ZOD-MULTI-ISSUE — `returnAllZodIssues` shared helper +
`/api/dashboard/widgets` 422 now returns every Zod issue + worker
audit-ledger breadcrumb. 41 sibling routes documented for v1.4.43.
- W3-QUERYKEY-LONGTAIL — 40 settings/medications/admin/hooks files
migrated to the queryKeys factory; 9 new factory entries; ESLint
rule + test-guard scope extended in lockstep.
- W4-ARCH-HYGIENE — BERLIN_DAY_FORMATTER consolidated across nine
sites (seven status helpers + bucket-series + bp-in-target);
Suspense double-comment consolidated; doctor-report sanitiser regex
rewritten to escape-form so diffs are reviewable; pr-detection-
worker soft-delete filter; offhost-backup DR-intent comment.
- W5-IOS-WORKOUTS — `dedupeWorkoutBatch` write-time cross-source
dedup helper for the v1.5 iOS HealthKit observer ingest; dashboard
tile-strip placeholder parity with the live TrendCard chrome.
- W6-WITHINGS-OFF + ENV-CHECK — Withings off-response classifier
(transient/reauth/persistent) + WithingsApiError typed throw + the
`pnpm check-env` CLI. APNs `allOrNone: true` closes the AP-2
silent-disable gap the wave was conceived to close.
No migration, no env-var change, no API contract break for iOS v0.5.4.
The new `/api/dashboard/widgets` 422 envelope is additively extended.
pnpm test --run green at 4815 passing / 1 skipped.
pnpm typecheck, pnpm lint, pnpm knip (no --include flag) green.
Server-side prep for the next iOS TestFlight build. Five surfaces land together so the iOS engineer can pick them up in one cut-over: a locked externalId shape for daily-aggregated cumulative HealthKit rows, the SyncMode foundation columns + handshake + bulk-backfill endpoints, a first-class MoodEntry.note column that replaces the legacy tag-based workaround, a cross-source workout dedup helper, and two new MeasurementType enums plus the shared category overlay that drives the iOS permission picker and the future Insights nav.
Largest feature delta in the v1.4.x line. Insights expands into seven dedicated metric sub-pages; GLP-1 medication tracking lands end-to-end (drug-level chart with Research Mode acknowledgment, EMA titration ladder, cadence + compliance, side-effect taxonomy, pen/vial inventory); cross-source priority becomes a two-axis resolver; per-user timezone threads through ten analytics surfaces; Withings activity + sleep v2 sync; onboarding rebuilt as a nested-route wizard; Personal Records ship end-to-end; Health Score provenance accordion; Coach runs native first-party prompts across six locales with a 1800-assertion refusal-probe matrix; OpenAPI drift gate hard-fails; multi-arch GHCR image. Migrations 0043 through 0060 are additive and forward-only. PROMPT_VERSION 4.24.0 → 4.25.0 with GROUND RULE 9 (refuses dose recommendations) and GROUND RULE 15 (refuses drug-level estimates with MDR + MDCG 2021-24 cites).
See CHANGELOG.md for the full per-section delta.