feat(insights): adopt the canonical per-metric detail template and terminology

Bring the web per-metric Insights detail pages to structural parity with
the canonical metric template: a single top-to-bottom spine, every block
self-suppressing when its data is absent.

New spine (above the chart): header (title + target gear + Coach circle)
→ intro → primary tile → Letzte Messung → stat strip; then the chart,
the range controls + period delta, the per-metric correlation card, and
the Einschätzung assessment last.

- Add the primary tile (headline value + 30-day average + an In-range
  bar shown only when the metric carries a per-metric in-target pct).
  Blood pressure keeps its richer target panel as its primary tile.
- Add the Letzte Messung card (labeled heading + last-reading date),
  reading the last-seen timestamp the slim analytics slice already
  carries — no extra fetch.
- Relocate the owning correlation onto its metric page: weight × weekday
  on Weight, mood × pulse on Pulse. The card reads the correlations
  already on the analytics payload and self-gates below the surfacing
  bar; it is now the canonical home for those relationships.
- Move the headline value, 30-day average and in-target share out of the
  target reference panel so each figure appears in exactly one place;
  the panel stays the band reference (range, status, positional bar,
  consistency strip, source). Blood pressure keeps its stitched S/D
  30-day average inline.
- Order the Einschätzung last on every page.

Terminology: adopt the canonical labels — rename the mood patterns
surface to "Was auffällt", pluralise the deviation-count card to
"Signale des Tages", and align the settings section labels to
"Über diese App" and "Quellen-Priorität". New In-range /
30-day-average / Letzte Messung strings land in all six locales.

Web charts are unchanged — Recharts and the existing chart tokens
stay; the iOS monochrome chart doctrine is not applied.
This commit is contained in:
Marc-André Bombeck committed 2026-06-04 23:03:05 +02:00
1 parent 6ad0a8a5cf
commit d5eb887757
19 files changed
+564 -78

No files matched your search

+16 -3
View File
@@ -13,6 +13,7 @@ import { HealthChartDynamic } from "@/components/charts/health-chart-dynamic";
import { InsightStatusCard } from "@/components/insights/insight-status-card";
import { MeasurementDiversityNudge } from "@/components/insights/measurement-diversity-nudge";
import { MetricEmptyState } from "@/components/insights/metric-empty-state";
import { MetricLastMeasurementCard } from "@/components/insights/metric-last-measurement-card";
import { MetricRangeControls } from "@/components/insights/metric-range-controls";
import { MetricTargetSummary } from "@/components/insights/metric-target-summary";
import { SubPageShell } from "@/components/insights/sub-page-shell";
@@ -46,7 +47,10 @@ export default function InsightsBlutdruckPage() {
const { data: status, isLoading: isStatusLoading } =
useInsightStatus("blood-pressure");
const { isEmpty } = useInsightsAnalytics("BLOOD_PRESSURE_SYS");
const { data: analytics, isEmpty } =
useInsightsAnalytics("BLOOD_PRESSURE_SYS");
const bpLastSeenAt =
analytics?.lastSeenByType?.BLOOD_PRESSURE_SYS?.lastSeenAt ?? null;
if (isEmpty) {
return (
@@ -103,6 +107,15 @@ export default function InsightsBlutdruckPage() {
description={t("insights.subPage.blutdruckDescription")}
explainerMetric="bloodPressure"
coachLaunch
primary={
<>
{/* Blood pressure's primary tile IS its richer target panel
(ESH classification + sys/dia band + 30-day S/D average),
promoted above the chart per the canonical spine. */}
<MetricTargetSummary slug="blood-pressure" />
<MetricLastMeasurementCard lastSeenAt={bpLastSeenAt} />
</>
}
diversityNudge={
<MeasurementDiversityNudge
measurementType="BLOOD_PRESSURE_SYS"
@@ -132,8 +145,8 @@ export default function InsightsBlutdruckPage() {
{/* v1.12.0 — range pills + period-over-period delta below the chart. */}
<MetricRangeControls measurementType="BLOOD_PRESSURE_SYS" enabled={!isEmpty} />
<MetricTargetSummary slug="blood-pressure" />
{/* v1.12.0 — Einschätzung is the last block on the canonical
metric-detail spine. */}
<InsightStatusCard
title={t("insights.assessmentTitle")}
icon={<HeartPulse className="h-5 w-5" />}
+27 -15
View File
@@ -13,6 +13,9 @@ import { HealthChartDynamic } from "@/components/charts/health-chart-dynamic";
import { InsightStatusCard } from "@/components/insights/insight-status-card";
import { MetricEmptyState } from "@/components/insights/metric-empty-state";
import { MetricStatStrip } from "@/components/insights/metric-stat-strip";
import { MetricPrimaryTile } from "@/components/insights/metric-primary-tile";
import { MetricLastMeasurementCard } from "@/components/insights/metric-last-measurement-card";
import { MetricCorrelationCard } from "@/components/insights/metric-correlation-card";
import { MeasurementDiversityNudge } from "@/components/insights/measurement-diversity-nudge";
import { MetricRangeControls } from "@/components/insights/metric-range-controls";
import { MetricTargetSummary } from "@/components/insights/metric-target-summary";
@@ -52,6 +55,8 @@ export default function InsightsPulsPage() {
// the same key).
const { data: analytics, isEmpty } = useInsightsAnalytics("PULSE");
const vo2Summary = analytics?.summaries?.VO2_MAX ?? null;
const pulseSummary = analytics?.summaries?.PULSE ?? null;
const pulseLastSeenAt = analytics?.lastSeenByType?.PULSE?.lastSeenAt ?? null;
// v1.4.27 F17 — gate the sub-page on at least one pulse observation.
// Brand-new accounts (no manual logs, no Apple-Health upload yet)
@@ -116,12 +121,13 @@ export default function InsightsPulsPage() {
title={t("insights.pulseSectionTitle")}
description={t("insights.subPage.pulsDescription")}
explainerMetric="pulse"
statStrip={
<MetricStatStrip
summary={analytics?.summaries?.PULSE ?? null}
unit="bpm"
/>
primary={
<>
<MetricPrimaryTile summary={pulseSummary} unit="bpm" slug="pulse" />
<MetricLastMeasurementCard lastSeenAt={pulseLastSeenAt} />
</>
}
statStrip={<MetricStatStrip summary={pulseSummary} unit="bpm" />}
diversityNudge={
<MeasurementDiversityNudge
measurementType="PULSE"
@@ -147,16 +153,6 @@ export default function InsightsPulsPage() {
<MetricTargetSummary slug="pulse" />
<InsightStatusCard
title={t("insights.assessmentTitle")}
icon={<Heart className="h-5 w-5" />}
text={status?.text ?? null}
hasProvider={status?.hasProvider ?? false}
updatedAt={status?.updatedAt ?? null}
loading={isStatusLoading}
preparing={status?.preparing ?? false}
/>
{/* v1.4.25 W16a — VO2 max sits on the cardio sub-page because it
is a cardio-fitness metric (Apple's Health app surfaces it
under "Heart"). The chart-row stays mounted even at zero
@@ -168,6 +164,22 @@ export default function InsightsPulsPage() {
compareBaseline={compareBaseline}
userTimezone={user?.timezone}
/>
{/* v1.12.0 — Pulse owns the mood × pulse correlation (relocated off
the overview onto its metric page). */}
<MetricCorrelationCard slug="pulse" />
{/* v1.12.0 — Einschätzung is the last block on the canonical
metric-detail spine. */}
<InsightStatusCard
title={t("insights.assessmentTitle")}
icon={<Heart className="h-5 w-5" />}
text={status?.text ?? null}
hasProvider={status?.hasProvider ?? false}
updatedAt={status?.updatedAt ?? null}
loading={isStatusLoading}
preparing={status?.preparing ?? false}
/>
</SubPageShell>
);
}
+6 -1
View File
@@ -10,6 +10,7 @@ import { Button } from "@/components/ui/button";
import { MeasurementDiversityNudge } from "@/components/insights/measurement-diversity-nudge";
import { MetricStatusCard } from "@/components/insights/metric-status-card";
import { MetricEmptyState } from "@/components/insights/metric-empty-state";
import { MetricLastMeasurementCard } from "@/components/insights/metric-last-measurement-card";
import { MetricRangeControls } from "@/components/insights/metric-range-controls";
import { MetricTargetSummary } from "@/components/insights/metric-target-summary";
import { SleepOverview } from "@/components/insights/sleep-overview";
@@ -35,7 +36,10 @@ export default function InsightsSchlafPage() {
const { t } = useTranslations();
const { user } = useAuth();
const { isEmpty } = useInsightsAnalytics("SLEEP_DURATION");
const { data: analytics, isEmpty } =
useInsightsAnalytics("SLEEP_DURATION");
const sleepLastSeenAt =
analytics?.lastSeenByType?.SLEEP_DURATION?.lastSeenAt ?? null;
if (isEmpty) {
return (
@@ -67,6 +71,7 @@ export default function InsightsSchlafPage() {
description={t("insights.sleep.description")}
explainerMetric="sleep"
coachLaunch
primary={<MetricLastMeasurementCard lastSeenAt={sleepLastSeenAt} />}
diversityNudge={
<MeasurementDiversityNudge
measurementType="SLEEP_DURATION"
+15
View File
@@ -13,6 +13,9 @@ import { HealthChartDynamic } from "@/components/charts/health-chart-dynamic";
import { InsightStatusCard } from "@/components/insights/insight-status-card";
import { MetricEmptyState } from "@/components/insights/metric-empty-state";
import { MetricStatStrip } from "@/components/insights/metric-stat-strip";
import { MetricPrimaryTile } from "@/components/insights/metric-primary-tile";
import { MetricLastMeasurementCard } from "@/components/insights/metric-last-measurement-card";
import { MetricCorrelationCard } from "@/components/insights/metric-correlation-card";
import { MeasurementDiversityNudge } from "@/components/insights/measurement-diversity-nudge";
import { MetricRangeControls } from "@/components/insights/metric-range-controls";
import { MetricTargetSummary } from "@/components/insights/metric-target-summary";
@@ -42,6 +45,8 @@ export default function InsightsGewichtPage() {
const { data: analytics, isEmpty } = useInsightsAnalytics("WEIGHT");
const weightSummary = analytics?.summaries?.WEIGHT ?? null;
const weightLastSeenAt =
analytics?.lastSeenByType?.WEIGHT?.lastSeenAt ?? null;
if (isEmpty) {
return (
@@ -79,6 +84,12 @@ export default function InsightsGewichtPage() {
title={t("insights.weightSectionTitle")}
description={t("insights.subPage.gewichtDescription")}
explainerMetric="weight"
primary={
<>
<MetricPrimaryTile summary={weightSummary} unit="kg" slug="weight" />
<MetricLastMeasurementCard lastSeenAt={weightLastSeenAt} />
</>
}
statStrip={<MetricStatStrip summary={weightSummary} unit="kg" />}
diversityNudge={
<MeasurementDiversityNudge
@@ -113,6 +124,10 @@ export default function InsightsGewichtPage() {
compact
/>
{/* v1.12.0 — Weight owns the weight × weekday correlation (relocated
off the overview onto its metric page). */}
<MetricCorrelationCard slug="weight" />
<InsightStatusCard
title={t("insights.assessmentTitle")}
icon={<Scale className="h-5 w-5" />}