feat(insights): add the vitals dashboard + its derived metric tier

Build the Apple-Health-Highlights grid of personal-vital tiles on the
Insights overview, backed by four new pure derived-metric compute
functions over the established seam:

- cardio-fitness band ("Fitness Age" frame) — VO2max placed against the
  FRIEND-registry age/sex norm (Kaminsky 2015), surfaced + trended, never
  re-derived.
- vascular-age delta — Withings vascular-age vs chronological age, a
  relative wellness indicator (Vlachopoulos 2010 PWV framing), no medical
  claim.
- HRV (SDNN) balance — reuses the typical-range engine for the band and
  places the recent average as balanced / above / below; SDNN never
  relabelled RMSSD; cadence-gated.
- BMI from weight + height — exact WHO classification, the fallback when
  no device BMI exists.

Each metric carries coverage + confidence + provenance and cites its
standard through the provenance explainer. The dashboard renders one
SparklineDeltaTile per available vital: an absent vital does not render,
a still-building one shows the honest coverage meter rather than a
headline from too little history. Tiles read the generic
/api/insights/derived route through a TanStack Query hook keyed off the
centralised factory.

Regenerates the OpenAPI contract for the two new metric ids and adds the
vitals copy across all six locales.
This commit is contained in:
Marc-André Bombeck committed 2026-06-02 21:17:05 +02:00
1 parent 32bc11f968
commit 663a3ee09d
28 files changed
+1742 -14

No files matched your search

+2 -1
View File
@@ -67,7 +67,7 @@ export const GET = apiHandler(async (request: NextRequest) => {
// re-fetched per metric — the pool-contention mitigation).
const profile = await prisma.user.findUnique({
where: { id: user.id },
select: { dateOfBirth: true, gender: true },
select: { dateOfBirth: true, gender: true, heightCm: true },
});
const sex =
profile?.gender === "MALE" || profile?.gender === "FEMALE"
@@ -80,6 +80,7 @@ export const GET = apiHandler(async (request: NextRequest) => {
profile: {
ageYears: getAgeFromDateOfBirth(profile?.dateOfBirth ?? null),
sex,
heightCm: profile?.heightCm ?? null,
},
type: parsed.data.type ?? null,
});