fix(insights): wire grip / pain / waist / waist-to-height across web and MCP

Grip strength, pain (0–10 NRS), waist circumference, and waist-to-height
existed in the measurement enum, signal registry, and FHIR export but had
no web entry, no detail page, single non-sex-aware bands, and a split-brain
MCP exposure. This closes those gaps.

Web capture: the four signals join the manual measurement form, so a
web-only self-hoster can log them (the iOS client and the measurements API
already could).

Detail pages: flip detailPage on for the four signals and mount each on the
shared HealthKit metric template (chart, stat strip, Coach-read strip,
assessment card) via routed sub-pages and tab-strip pills, the same detail
spine every other metric carries.

Sex-aware bands: grip strength (EWGSOP2 men 27 / women 16 kg) and waist
(WHO men 94 / women 80 cm) now classify by the profile sex through the
age/sex norm resolver the assessment already threads; the resolver also
returns a sex-specific, age-independent band when no date of birth is set.
Fix the waist-to-height boundary so the NICE >= 0.5 rule is correct (the
last in-range value is 0.49).

MCP single source of truth: decouple the registry mcp facet from the Coach
snapshot — a signal can be MCP-readable without being on the Coach surface.
Mark the four physical signals mcp:true, derive the MCP rich-reads clinical
set from the registry flag instead of a duplicate allowlist, and relax the
invariant to coachSnapshot implies mcp (not the reverse). PHQ-9 / GAD-7 and
the environmental signals stay mcp:false and unreachable. compare_metric and
detect_changepoints now advertise clinical-signal support to match
get_metric_baseline.
This commit is contained in:
Marc-André Bombeck committed 2026-06-28 18:33:53 +02:00
1 parent 4f39058629
commit a69697b50f
25 files changed
+726 -62

No files matched your search

+5 -1
View File
@@ -87,7 +87,11 @@ const layoutSchema = z.object({
}),
)
.min(1)
.max(50)
// Cap at the accepted-id universe (canonical slugs + legacy aliases) so a
// single PUT can cover every tile the layout knows about — the cap grows
// automatically as new sub-page slugs land, rather than tripping a 422
// once the slug count crosses a hard-coded ceiling.
.max(ACCEPTED_INSIGHTS_TILE_IDS.length)
.optional(),
});
+34
View File
@@ -0,0 +1,34 @@
"use client";
import { Dumbbell } from "lucide-react";
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
import { useTranslations } from "@/lib/i18n/context";
/**
* v1.25 — `/insights/grip-strength`.
*
* Grip-strength sub-page. Rides the generic HealthKit metric template so the
* signal gets the same detail spine as every other metric (chart, stat strip,
* Coach-read strip, assessment card). The assessment threads the sex-aware
* EWGSOP2 cut-off (men 27 / women 16 kg) via the `norms.ts` table.
*/
export default function InsightsGripStrengthPage() {
const { t } = useTranslations();
return (
<HealthKitMetricPage
measurementType="GRIP_STRENGTH"
statusMetric="GRIP_STRENGTH"
insightMetric="GRIP_STRENGTH"
chartKey="gripStrength"
i18nPrefix="insights.gripStrength"
color="#22c55e"
unit="kg"
yAxisUnit="kg"
statIcon={Dumbbell}
emptyStateIcon={<Dumbbell className="size-6" />}
emptyStateCtaType="GRIP_STRENGTH"
coachPrefill={t("insights.gripStrength.coachPrefill")}
/>
);
}
+33
View File
@@ -0,0 +1,33 @@
"use client";
import { Activity } from "lucide-react";
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
import { useTranslations } from "@/lib/i18n/context";
/**
* v1.25 — `/insights/pain`.
*
* Pain (0–10 NRS) sub-page. Patient-reported pain intensity on the validated
* numeric rating scale; rides the generic metric template for its detail
* spine + assessment (no-pain / mild band at 0–3).
*/
export default function InsightsPainPage() {
const { t } = useTranslations();
return (
<HealthKitMetricPage
measurementType="PAIN_NRS"
statusMetric="PAIN_NRS"
insightMetric="PAIN_NRS"
chartKey="painNrs"
i18nPrefix="insights.pain"
color="#f97316"
unit=""
yAxisUnit="/10"
statIcon={Activity}
emptyStateIcon={<Activity className="size-6" />}
emptyStateCtaType="PAIN_NRS"
coachPrefill={t("insights.pain.coachPrefill")}
/>
);
}
+32
View File
@@ -0,0 +1,32 @@
"use client";
import { Scaling } from "lucide-react";
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
import { useTranslations } from "@/lib/i18n/context";
/**
* v1.25 — `/insights/waist-to-height`.
*
* Waist-to-height ratio sub-page. The ancestry-robust single number (NICE:
* keep waist under half your height — WHtR ≥ 0.5 flags increased risk). Rides
* the generic metric template for its detail spine + assessment.
*/
export default function InsightsWaistToHeightPage() {
const { t } = useTranslations();
return (
<HealthKitMetricPage
measurementType="WAIST_TO_HEIGHT"
statusMetric="WAIST_TO_HEIGHT"
insightMetric="WAIST_TO_HEIGHT"
chartKey="waistToHeight"
i18nPrefix="insights.waistToHeight"
color="#8b5cf6"
unit=""
statIcon={Scaling}
emptyStateIcon={<Scaling className="size-6" />}
emptyStateCtaType="WAIST_TO_HEIGHT"
coachPrefill={t("insights.waistToHeight.coachPrefill")}
/>
);
}
+33
View File
@@ -0,0 +1,33 @@
"use client";
import { Ruler } from "lucide-react";
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
import { useTranslations } from "@/lib/i18n/context";
/**
* v1.25 — `/insights/waist`.
*
* Waist-circumference sub-page. Rides the generic metric template; the
* assessment threads the sex-aware WHO cut-off (men 94 / women 80 cm) via the
* `norms.ts` table.
*/
export default function InsightsWaistPage() {
const { t } = useTranslations();
return (
<HealthKitMetricPage
measurementType="WAIST_CIRCUMFERENCE"
statusMetric="WAIST_CIRCUMFERENCE"
insightMetric="WAIST_CIRCUMFERENCE"
chartKey="waistCircumference"
i18nPrefix="insights.waist"
color="#0ea5e9"
unit="cm"
yAxisUnit="cm"
statIcon={Ruler}
emptyStateIcon={<Ruler className="size-6" />}
emptyStateCtaType="WAIST_CIRCUMFERENCE"
coachPrefill={t("insights.waist.coachPrefill")}
/>
);
}