feat(insights): surface the recovery score's factor breakdown

The recovery detail page rendered only the ring — the one wellness
score without a composition view, although the COMPUTED score IS the
readiness blend persisted nightly. The wellness-score read now attaches
the blend's per-factor sub-scores (RHR, HRV, sleep, respiratory rate,
mood stability — same engine, same weights, resolved server-side on the
same request) when the canonical latest row is the COMPUTED proxy, and
the anatomy view renders them through the identical ranked contributor
rows the readiness and sleep-score composites use, reusing their
labels. A WHOOP-native percentage is not our blend, so it keeps the
row-less presentation; a gated blend simply omits the breakdown. The
new value field is optional and additive — existing clients decode
unchanged.
This commit is contained in:
Marc-André Bombeck committed 2026-07-05 14:58:49 +02:00
1 parent 75b0e618b4
commit dc963540b8
3 files changed
+102 -2

No files matched your search

@@ -185,10 +185,25 @@ export function CompositeScoreAnatomy({
}))
.sort(rankByImpact);
} else {
// Persisted nightly score — no sub-decomposition; ring + provenance.
// Persisted nightly score — ring + provenance. RECOVERY additionally
// carries the readiness-blend factor decomposition (v1.27.5, resolved
// server-side by the same engine that mints the nightly score), so it
// renders the same ranked contributor rows as the composites. The
// factor set IS the readiness component set, so the labels reuse the
// READINESS component keys verbatim. STRESS / STRAIN stay row-less.
const v = data.value as WellnessScoreValue;
score = v.score;
caption = t(`insights.derived.scoreRing.band.${v.band}`);
if (metric === "RECOVERY_SCORE" && v.components) {
contributors = v.components
.map((c) => ({
key: c.key,
label: t(`insights.derived.composite.READINESS.component.${c.key}`),
value: c.value,
weight: c.weight,
}))
.sort(rankByImpact);
}
}
}