v1.10.3 — personalised strain, a daily signal card, deeper derived metrics#249
Merged
Conversation
…signal" card Promote the COINCIDENT_DEVIATION flag from a fire-only tile buried in the below-the-fold vitals grid to an always-mounted headline card at the top of the Insights overview. The card renders four calm states off the single existing derived payload — building-baselines, all-clear, watch, fired — with no new engine math, route, or schema change. Keep it calm and clinically safe: at most an amber band, never red, no score, no chart, no push. Soften a fired flag to the watch tone when the backing history is thin. The fired/watch states carry the load-bearing "possible factors, never a cause / not a diagnosis" framing and reuse ProvenanceExplainer + CoverageMeter + the band tokens verbatim. Remove the now-redundant tile, its batch token, and the hasRenderableVital branch from the vitals grid so the signal is not shown twice.
Register four new derived metrics for the additive HealthKit signals: - WRIST_TEMPERATURE_BASELINE, STAIR_ASCENT_SPEED_BASELINE, STAIR_DESCENT_SPEED_BASELINE — any-user-baseline personal bands reusing the type-generic median +/- k*MAD engine via a new BASELINE_CAPABLE_TYPES allowlist. Wrist temperature is a personal-deviation band only (no illness or cycle inference); stair speeds are personal trend bands (no population cutoff, geometry-confounded). - SIX_MINUTE_WALK_BAND — passthrough re-frame placing the device-estimated distance against the Enright & Sherrill 1998 predicted 6MWD (ATS 2002 test standard), as percent-of-predicted with a green/yellow/red band; band null when age/height/weight/sex are incomplete. Fall count and the breathing-disturbance index stay trend-only by design, documented in the registry: fall count is a zero-inflated discrete safety event, the breathing index is a regulated screening signal where a derived band would imply a diagnosis. Note wrist temperature as a deferred correlation outcome channel held pending a privacy review (cycle-phase inference risk), mirroring the medication-compliance omission note. Add provenance + standards entries, method/caveat copy across all six locales, and tests for the engine routing, the Enright equation, and the percent-of-predicted band.
Map day-total Banister TRIMP to 0-100 against a personal reference -- the EWMA-smoothed 75th percentile of the user's own training-day TRIMP over a 42-day chronic window -- instead of the fixed population anchor that pinned a deconditioned or chronic-condition user near 0 regardless of their effort. Below a 7-training-day cold-start floor the engine falls back to the population anchor, recorded as anchor: population with lower confidence; at or above it the personal anchor takes over. Always writes a score row -- no new null gate. The anchor is derived from the TRIMP input, never the 0-100 output, so there is no circular feedback; rest-day zeros stay out of the training-day distribution. Add a server-internal per-(user, day) strain_trimp_cache (day-total TRIMP + running EWMA reference + anchor) so the chronic window reads cheap cached values instead of re-integrating 42 days of HR series each night. The cache is not a Measurement -- it stays out of every user-facing surface, the doctor PDF, and the FHIR bundle by construction. No backfill: the nightly idempotent recompute populates the cache forward and self-heals, falling back to the population anchor until enough personal history accrues. Keep the Banister/Morton/Tanaka citations and the doctor-PDF exclusion; the honesty copy now states the scale is relative to the user's typical effort, with a general reference during cold start.
The recovery / stress / strain nightly finders used distinct + take with no order, so when more than the cap of users qualified the selected set was arbitrary across runs. Switch to a grouped query ordered by each user's newest input first with userId as the stable tiebreak, so the cap takes the most-recently-active users reproducibly. The strain finder interleaves its workout and active-energy lists so the merged cap favours recency across both sources.
…rics The Derived<T> tile (SparklineDeltaTile) renders a trailing series, but the engines never filled it, so the inline sparkline was always empty. Each engine now carries a capped trailing series sourced from the rows it already reads: - baseline-band metrics (incl. wrist-temperature / stair ascent+descent) ship the per-day mean series the band is built from; - HRV balance reuses the day-mean read it already does for the recent average; - the wellness scores (recovery / stress / strain) ship their window scores; - the six-minute-walk band ships its reading series; - BMI reads a bounded recent-weight set and derives a BMI series at the fixed profile height. All series cap at the last 30 points. The vitals dashboard wires the series into the baseline, HRV and BMI tiles; the tile already renders >= 2 points and collapses the sparkline row otherwise.
The age/sex reference tables are coarse decade / paediatric brackets, so a fractional age read a hard step at a bracket edge (39.9 -> 30s band, 40.0 -> 40s band). Anchor each band at its bracket centre and linearly interpolate a fractional age between the two adjacent centres; below the youngest centre / above the oldest centre hold the nearest cited band flat (clamp, never extrapolate) so the band stays within the span of the two cited brackets and the standard's provenance stays accurate.
…eaders The readiness + coincident-deviation latest-day-mean reads cap the row scan on a dense intra-day day. Lift the bare 50 into a named MAX_LATEST_DAY_ROWS constant and document the dense-intraday-retention reasoning at both call sites so the bound is intentional rather than a loose literal.
The sleep midpoint was minutes-of-day in UTC, so a non-UTC sleeper's consistency and timing sub-scores drifted with the offset (a 03:00-local midpoint read as a different clock time than a UTC user's). Resolve the user's stored zone in computeSleepScore and express the midpoint against it via the existing tz helpers; reconstructNights takes an explicit tz (UTC default) so the pure path stays test-pinnable.
The additive HealthKit baselines dispatch via an explicit per-metric type map, so the allowlist helper and its constant had no consumer.
The personal-relative-anchor explanation (own recent training-day load vs a general reference while building history) was EN-only; de/es/fr/it/pl carried the stale v1.10.0 method+description. Port both strings to all five so the two-mode honesty distinction is visible regardless of locale. Also correct the stale strain-score header comment: STRAIN is not excluded from the doctor PDF — it rides a segregated, disclaimed Wellness-summary section.
The estimated 6-minute-walk band + the three any-user HealthKit baseline bands (wrist temperature, stair ascent/descent speed) were registered, provenance-mapped and engine-tested but rendered on no surface. Add a "Mobility & body" section to the vitals dashboard: the 6MWT passthrough re-frame (distance + trend always, percent-of-predicted framing only with the Enright demographics) and a shared baseline-band tile for the three bands. Every tile is data-availability-gated like the existing ones — absent with no readings, CoverageMeter while provisional, value + framing + provenance when ready — and the section heading hides when none resolve. All four tokens ride the single batched derived request. Derive the sparkline gradient id via useId() to avoid duplicate-label collisions.
The dynamic loader skeleton reserved 120px while the resolved card's insufficient (~168px) and fired (~192px) states ran taller, shifting the hero and everything below it down 48–72px on resolve. Pin a min-h-48 on the card shell sized to the tallest realistic state, and match both the chunk-loader skeleton and the in-component CardSkeleton to that footprint (including the 44px provenance header row) so there is zero shift across loading → any state. Announce the fired state to screen readers via a polite role=status (not the assertive role=alert), and mark the decorative loader skeleton aria-hidden.
…eeper derived metrics
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Patch release: a research-led round of derived-metrics depth + Insights information-architecture, all additive and clinically conservative.
Added
Changed
strain_trimp_cachetable (migration 0109, additive).API
/api/insights/derived+/api/meta/capabilitiesgain four derived ids (WRIST_TEMPERATURE_BASELINE,STAIR_ASCENT_SPEED_BASELINE,STAIR_DESCENT_SPEED_BASELINE,SIX_MINUTE_WALK_BAND) — additive, forward-compatible, the wellness-score read shape is unchanged.Verification