diff --git a/src/app/insights/blood-glucose/page.tsx b/src/app/insights/blood-glucose/page.tsx
index c71a5e9fa..d87b52c80 100644
--- a/src/app/insights/blood-glucose/page.tsx
+++ b/src/app/insights/blood-glucose/page.tsx
@@ -25,6 +25,7 @@ export default function InsightsBlutzuckerPage() {
yAxisUnit="mg/dL"
emptyStateIcon={}
emptyStateCtaType={null}
+ targetSummarySlug="blood-glucose"
coachPrefill="I haven't logged any blood glucose yet — what does this metric tell me about my health, and how do I improve it?"
/>
);
diff --git a/src/app/targets/page.tsx b/src/app/targets/page.tsx
index 2775fd27b..9d6a9e49a 100644
--- a/src/app/targets/page.tsx
+++ b/src/app/targets/page.tsx
@@ -14,6 +14,7 @@ import {
type TargetCardData,
} from "@/components/targets/target-card";
import { TargetsSummaryHeader } from "@/components/targets/targets-summary-header";
+import { getTargetSourceLink } from "@/lib/targets/source-link";
import { CoachDrawer } from "@/components/insights/coach-panel/coach-drawer";
import { useCoachHandoff } from "@/hooks/use-coach-handoff";
import { useFeatureFlags } from "@/hooks/use-feature-flags";
@@ -92,41 +93,6 @@ function sortKey(target: TargetData): number {
return FIXED_TARGET_ORDER[target.type] ?? 100;
}
-/**
- * Source-citation URL for each target type. Kept on the page (rather
- * than the card) so the card stays presentational and the routing
- * stays declarative.
- */
-function getTargetSourceLink(target: TargetData): string | null {
- if (target.type === "WEIGHT" || target.type === "BMI") {
- return "https://www.who.int/news-room/fact-sheets/detail/obesity-and-overweight";
- }
- if (
- target.type === "BLOOD_PRESSURE" ||
- target.type === "BLOOD_PRESSURE_IN_TARGET"
- ) {
- return "https://academic.oup.com/eurheartj/article/39/33/3021/5079119";
- }
- if (target.type === "PULSE") {
- if (target.source.includes("CDC/NCHS")) {
- return "https://www.cdc.gov/nchs/data/nhsr/nhsr041.pdf";
- }
- if (target.source.includes("AHA")) {
- return "https://www.heart.org/en/health-topics/high-blood-pressure/the-facts-about-high-blood-pressure/all-about-heart-rate-pulse";
- }
- }
- if (target.type === "SLEEP_DURATION") {
- return "https://aasm.org/seven-or-more-hours-of-sleep-per-night-a-health-necessity-for-adults/";
- }
- if (target.type === "BODY_FAT") {
- return "https://www.acefitness.org/resources/everyone/blog/6596/what-are-the-guidelines-for-percentage-of-body-fat-loss/";
- }
- if (target.type === "ACTIVITY_STEPS") {
- return "https://www.who.int/publications/i/item/9789240015128";
- }
- return null;
-}
-
export default function TargetsPage() {
const { t } = useTranslations();
const { isAuthenticated, user } = useAuth();
diff --git a/src/components/insights/__tests__/metric-target-summary.test.tsx b/src/components/insights/__tests__/metric-target-summary.test.tsx
index 0d3ed5aa4..0ff5bbaf9 100644
--- a/src/components/insights/__tests__/metric-target-summary.test.tsx
+++ b/src/components/insights/__tests__/metric-target-summary.test.tsx
@@ -4,15 +4,18 @@ import { renderToStaticMarkup } from "react-dom/server";
import { I18nProvider } from "@/lib/i18n/context";
/**
- * v1.8.0 — `` unit tests.
+ * v1.8.0 → v1.8.5 — `` unit tests.
*
- * The card surfaces the numeric target range + the in-target share on
- * each insights category page, reading the same `/api/insights/targets`
- * payload that powers `/targets`. The component depends on `useAuth`
- * (gate) and TanStack Query (`useQuery`) for the payload, so both are
- * mocked here and the assertions run through SSR — the load-bearing
- * behaviour is the range/share rendering + the slug→type mapping, not
- * the live fetch.
+ * The panel surfaces the numeric target range, the verbal status pill,
+ * the guideline source, the in-target share, the 30-day average, the
+ * range bar and the 7-day consistency strip on each insights category
+ * page, reading the same `/api/insights/targets` payload that powers
+ * `/targets`. The component depends on `useAuth` (gate) and TanStack
+ * Query (`useQuery`) for the payload, so both are mocked here and the
+ * assertions run through SSR — the load-bearing behaviour is the
+ * range/share/source rendering + the slug→type mapping (including the
+ * blood-glucose per-context split + unit conversion), not the live
+ * fetch.
*/
vi.mock("@/hooks/use-auth", () => ({
@@ -26,17 +29,31 @@ vi.mock("@tanstack/react-query", () => ({
const { MetricTargetSummary } = await import("../metric-target-summary");
+interface TargetFixtureItem {
+ type: string;
+ label: string;
+ current: number | null;
+ average30: number | null;
+ unit: string;
+ range: { min: number; max: number } | null;
+ classification: { category: string; color: string } | null;
+ source: string;
+ daysInRange7d: number;
+ daysLogged7d: number;
+ daysInRange30d: number;
+ daysLogged30d: number;
+ insufficientData: boolean;
+ consistency7d: ReadonlyArray<"in" | "near" | "out" | null>;
+}
+
interface TargetsFixture {
- targets: Array<{
- type: string;
+ targets: TargetFixtureItem[];
+ bpDiastolic: {
current: number | null;
- unit: string;
+ average30: number | null;
range: { min: number; max: number } | null;
- daysInRange30d: number;
- daysLogged30d: number;
- insufficientData: boolean;
- }>;
- bpDiastolic: { current: number | null; range: { min: number; max: number } | null };
+ };
+ profile?: { glucoseUnit?: string | null };
}
function renderWith(slug: string, data: TargetsFixture | undefined) {
@@ -48,34 +65,87 @@ function renderWith(slug: string, data: TargetsFixture | undefined) {
);
}
+const WEIGHT_ITEM: TargetFixtureItem = {
+ type: "WEIGHT",
+ label: "Weight",
+ current: 72,
+ average30: 71.4,
+ unit: "kg",
+ range: { min: 60.1, max: 80.9 },
+ classification: { category: "Normal", color: "#50fa7b" },
+ source: "WHO BMI",
+ daysInRange7d: 5,
+ daysLogged7d: 7,
+ daysInRange30d: 18,
+ daysLogged30d: 24,
+ insufficientData: false,
+ consistency7d: ["in", "in", "near", "in", "out", "in", "in"],
+};
+
const WEIGHT_DATA: TargetsFixture = {
- targets: [
- {
- type: "WEIGHT",
- current: 72,
- unit: "kg",
- range: { min: 60.1, max: 80.9 },
- daysInRange30d: 18,
- daysLogged30d: 24,
- insufficientData: false,
- },
- ],
- bpDiastolic: { current: null, range: null },
+ targets: [WEIGHT_ITEM],
+ bpDiastolic: { current: null, average30: null, range: null },
};
const BP_DATA: TargetsFixture = {
targets: [
{
type: "BLOOD_PRESSURE",
+ label: "Blood pressure",
current: 124,
+ average30: 122,
unit: "mmHg",
range: { min: 120, max: 129 },
+ classification: { category: "Optimal", color: "#50fa7b" },
+ source: "ESH 2023",
+ daysInRange7d: 3,
+ daysLogged7d: 6,
daysInRange30d: 10,
daysLogged30d: 20,
insufficientData: false,
+ consistency7d: ["in", "near", "in", "out", "in", "near", "in"],
},
],
- bpDiastolic: { current: 78, range: { min: 70, max: 79 } },
+ bpDiastolic: { current: 78, average30: 76, range: { min: 70, max: 79 } },
+};
+
+const GLUCOSE_DATA: TargetsFixture = {
+ targets: [
+ {
+ type: "BLOOD_GLUCOSE_FASTING",
+ label: "targets.glucoseFasting",
+ current: 95,
+ average30: 92,
+ unit: "mg/dL",
+ range: { min: 70, max: 100 },
+ classification: { category: "Optimal", color: "#50fa7b" },
+ source: "ADA 2024 / DDG",
+ daysInRange7d: 4,
+ daysLogged7d: 5,
+ daysInRange30d: 12,
+ daysLogged30d: 15,
+ insufficientData: false,
+ consistency7d: ["in", "in", "near", "in", "in", "in", "in"],
+ },
+ {
+ type: "BLOOD_GLUCOSE_POSTPRANDIAL",
+ label: "targets.glucosePostprandial",
+ current: 140,
+ average30: 138,
+ unit: "mg/dL",
+ range: { min: 70, max: 140 },
+ classification: { category: "Optimal", color: "#50fa7b" },
+ source: "ADA 2024 / DDG",
+ daysInRange7d: 3,
+ daysLogged7d: 4,
+ daysInRange30d: 9,
+ daysLogged30d: 12,
+ insufficientData: false,
+ consistency7d: ["in", "near", "in", "in", null, "in", "in"],
+ },
+ ],
+ bpDiastolic: { current: null, average30: null, range: null },
+ profile: { glucoseUnit: "mg/dL" },
};
describe("", () => {
@@ -87,10 +157,35 @@ describe("", () => {
expect(html).toContain("75% of logged days within target");
});
+ it("renders the verbal status pill + the guideline source", () => {
+ const html = renderWith("weight", WEIGHT_DATA);
+ expect(html).toContain('data-slot="target-status-pill"');
+ // EN label for the "Normal" classification category.
+ expect(html).toContain("Normal");
+ // Source citation + external-link.
+ expect(html).toContain("Source: WHO BMI");
+ // The WHO obesity fact-sheet URL resolves for WEIGHT.
+ expect(html).toContain(
+ "who.int/news-room/fact-sheets/detail/obesity-and-overweight",
+ );
+ });
+
+ it("renders the range bar + the 30-day average + the consistency strip", () => {
+ const html = renderWith("weight", WEIGHT_DATA);
+ expect(html).toContain('data-slot="target-range-bar"');
+ expect(html).toContain('data-slot="consistency-strip"');
+ expect(html).toContain("30-day average: 71.4 kg");
+ });
+
it("stitches the systolic + diastolic bands for blood pressure", () => {
const html = renderWith("blood-pressure", BP_DATA);
expect(html).toContain("Target: 120–129 / 70–79 mmHg");
expect(html).toContain("50% of logged days within target");
+ // The diastolic range bar renders alongside the systolic one.
+ const barCount = (html.match(/data-slot="target-range-bar"/g) ?? []).length;
+ expect(barCount).toBe(2);
+ // 30-day average stitches the diastolic pair.
+ expect(html).toContain("30-day average: 122/76 mmHg");
});
it("renders the adjust-target link to /targets", () => {
@@ -99,13 +194,14 @@ describe("", () => {
expect(html).toContain("Adjust target range");
});
- it("suppresses the share when the route flags insufficient data", () => {
+ it("suppresses the share + strip when the route flags insufficient data", () => {
const html = renderWith("weight", {
...WEIGHT_DATA,
- targets: [{ ...WEIGHT_DATA.targets[0], insufficientData: true }],
+ targets: [{ ...WEIGHT_ITEM, insufficientData: true }],
});
expect(html).toContain("Target: 60.1–80.9 kg");
expect(html).not.toContain("of logged days within target");
+ expect(html).not.toContain('data-slot="consistency-strip"');
});
it("renders nothing for a slug without a numeric target", () => {
@@ -121,8 +217,48 @@ describe("", () => {
it("renders nothing when the matching target has no range", () => {
const html = renderWith("weight", {
...WEIGHT_DATA,
- targets: [{ ...WEIGHT_DATA.targets[0], range: null }],
+ targets: [{ ...WEIGHT_ITEM, range: null }],
});
expect(html).toBe("");
});
+
+ describe("blood-glucose per-context split", () => {
+ it("renders one panel per logged glucose context in mg/dL", () => {
+ const html = renderWith("blood-glucose", GLUCOSE_DATA);
+ const panelCount = (
+ html.match(/data-slot="metric-target-summary"/g) ?? []
+ ).length;
+ expect(panelCount).toBe(2);
+ // mg/dL display unit keeps the canonical values unchanged.
+ expect(html).toContain("Target: 70–100 mg/dL");
+ expect(html).toContain("Target: 70–140 mg/dL");
+ // ADA / DDG source surfaces for each panel.
+ expect(html).toContain("Source: ADA 2024 / DDG");
+ // The i18n label keys resolve to EN context headings (rendered
+ // upper-cased by the heading style but kept as text content).
+ expect(html).toContain("Glucose — fasting");
+ expect(html).toContain("Glucose — post-meal");
+ });
+
+ it("converts the glucose bands to mmol/L when the profile prefers it", () => {
+ const html = renderWith("blood-glucose", {
+ ...GLUCOSE_DATA,
+ profile: { glucoseUnit: "mmol/L" },
+ });
+ // 70 mg/dL → 3.9, 100 mg/dL → 5.5, 140 mg/dL → 7.8 mmol/L
+ // (the /targets converter rounds to one decimal).
+ expect(html).toContain("Target: 3.9–5.5 mmol/L");
+ expect(html).toContain("Target: 3.9–7.8 mmol/L");
+ expect(html).toContain("mmol/L");
+ });
+
+ it("renders nothing when no glucose context has a band", () => {
+ const html = renderWith("blood-glucose", {
+ targets: [],
+ bpDiastolic: { current: null, average30: null, range: null },
+ profile: { glucoseUnit: "mg/dL" },
+ });
+ expect(html).toBe("");
+ });
+ });
});
diff --git a/src/components/insights/healthkit-metric-page.tsx b/src/components/insights/healthkit-metric-page.tsx
index 2b0f0ad0d..6394144d5 100644
--- a/src/components/insights/healthkit-metric-page.tsx
+++ b/src/components/insights/healthkit-metric-page.tsx
@@ -15,6 +15,7 @@ import { HealthChartDynamic } from "@/components/charts/health-chart-dynamic";
import { MetricEmptyState } from "@/components/insights/metric-empty-state";
import { MetricStatStrip } from "@/components/insights/metric-stat-strip";
import { MeasurementDiversityNudge } from "@/components/insights/measurement-diversity-nudge";
+import { MetricTargetSummary } from "@/components/insights/metric-target-summary";
import { SubPageShell } from "@/components/insights/sub-page-shell";
/**
@@ -88,6 +89,13 @@ export interface HealthKitMetricPageProps {
* Resolves `insights.subPage.explainer.{Title,Body}`.
*/
explainerMetric?: string;
+ /**
+ * v1.8.5 W5 — when set, renders `` beneath
+ * the chart. Used by blood glucose, whose per-context ADA / DDG bands
+ * live on the targets wire but whose page rides this generic scaffold
+ * rather than a bespoke module. Omit it for metrics without a target.
+ */
+ targetSummarySlug?: string;
}
export function HealthKitMetricPage({
@@ -104,6 +112,7 @@ export function HealthKitMetricPage({
coachPrefill,
valueScale,
explainerMetric,
+ targetSummarySlug,
}: HealthKitMetricPageProps) {
const { user, isAuthenticated } = useAuth();
const { t } = useTranslations();
@@ -186,6 +195,9 @@ export function HealthKitMetricPage({
userTimezone={user?.timezone}
valueScale={valueScale}
/>
+ {targetSummarySlug ? (
+
+ ) : null}
);
diff --git a/src/components/insights/metric-target-summary.tsx b/src/components/insights/metric-target-summary.tsx
index 9844d67e9..9508ff34a 100644
--- a/src/components/insights/metric-target-summary.tsx
+++ b/src/components/insights/metric-target-summary.tsx
@@ -2,14 +2,19 @@
import Link from "next/link";
import { useQuery } from "@tanstack/react-query";
-import { Target } from "lucide-react";
+import { ExternalLink, Target } from "lucide-react";
import { useAuth } from "@/hooks/use-auth";
import { queryKeys } from "@/lib/query-keys";
import { useTranslations } from "@/lib/i18n/context";
+import { convertGlucose, resolveGlucoseUnit } from "@/lib/glucose";
+import { RangeBar } from "@/components/targets/range-bar";
+import { ConsistencyStrip } from "@/components/targets/consistency-strip";
+import { TargetStatusPill } from "@/components/targets/target-status-pill";
+import { getTargetSourceLink } from "@/lib/targets/source-link";
/**
- * v1.8.0 — surface the numeric target range + in-target share on each
+ * v1.8.0 → v1.8.5 — surface the per-metric target reference panel on each
* insights category page, directly beneath the chart / assessment.
*
* The data is the same `/api/insights/targets` payload that powers the
@@ -19,22 +24,24 @@ import { useTranslations } from "@/lib/i18n/context";
* cache the `/targets` page then reuses. We never recompute the ranges
* here: the route is the single source of truth for the age-based ESH
* blood-pressure band, the WHO weight / BMI band, the Karvonen pulse
- * band, the AASM sleep band, and the medication / mood / step targets.
+ * band, the AASM sleep band, the ADA / DDG glucose bands, and the
+ * medication / mood targets.
*
- * Each insights slug maps to one target `type`. Blood pressure is the one
- * composite case: the route emits the systolic band on the
- * `BLOOD_PRESSURE` target and the diastolic band on the top-level
- * `bpDiastolic` slot, so we stitch them back into a `S/D` range string.
+ * v1.8.5 W5 — the panel grew from a one-line range card into a compact
+ * reference panel that ports the context the `/targets` card carries:
*
- * The "in target" share reads `daysInRange30d / daysLogged30d` — the
- * fraction of logged days over the last 30 whose mean reading landed in
- * the green band. We hide the share alone when the route flags
- * `insufficientData`; the range stays visible so the band is always
- * legible. This matches the `/targets` consistency strip so the two
- * surfaces never disagree.
+ * • Row 1: target range string + `` + source link
+ * (the "what is the band / what is it based on" answer).
+ * • Row 2: `` — where today's value sits inside the band
+ * (the spatial answer). BP renders the second diastolic bar.
+ * • Row 3: in-target share % + the 30-day average.
+ * • Row 4: `` — the last seven days at a glance.
+ * • Footer: the "Adjust target range" link to `/targets`.
*
- * A discreet "Adjust target range" link routes to `/targets`, which
- * stays the canonical place to view / tune the bands.
+ * Each insights slug maps to one target `type`, except blood glucose
+ * which maps to up to four per-context cards (fasting / postprandial /
+ * random / bedtime) — those carry mg/dL canonical values that we convert
+ * to the user's display unit exactly like the `/targets` page does.
*/
interface TargetRange {
@@ -42,22 +49,38 @@ interface TargetRange {
max: number;
}
+interface TargetClassification {
+ category: string;
+ color: string;
+}
+
interface TargetItem {
type: string;
+ label: string;
current: number | null;
+ average30: number | null;
unit: string;
range: TargetRange | null;
+ classification: TargetClassification | null;
+ source: string;
+ daysInRange7d: number;
+ daysLogged7d: number;
daysInRange30d: number;
daysLogged30d: number;
insufficientData: boolean;
+ consistency7d: ReadonlyArray<"in" | "near" | "out" | null>;
}
interface TargetsResponse {
targets: TargetItem[];
bpDiastolic: {
current: number | null;
+ average30: number | null;
range: TargetRange | null;
};
+ profile?: {
+ glucoseUnit?: string | null;
+ };
}
/**
@@ -65,6 +88,10 @@ interface TargetsResponse {
* Slugs without a numeric target (HRV, oxygen saturation, body
* temperature, active energy, workouts, resting HR) are absent — the
* component renders nothing for them, so the sub-page collapses cleanly.
+ *
+ * Blood glucose is the one slug that fans out to several target types:
+ * the route emits one card per logged context, so the summary renders a
+ * reference panel for each.
*/
const SLUG_TO_TARGET_TYPE: Record = {
"blood-pressure": "BLOOD_PRESSURE",
@@ -76,6 +103,14 @@ const SLUG_TO_TARGET_TYPE: Record = {
medications: "MEDICATION_COMPLIANCE",
};
+/** Glucose context target types, in the route's emission order. */
+const GLUCOSE_TARGET_TYPES = [
+ "BLOOD_GLUCOSE_FASTING",
+ "BLOOD_GLUCOSE_POSTPRANDIAL",
+ "BLOOD_GLUCOSE_RANDOM",
+ "BLOOD_GLUCOSE_BEDTIME",
+];
+
interface MetricTargetSummaryProps {
/** Insights category slug, e.g. `"blood-pressure"`. */
slug: string;
@@ -90,6 +125,7 @@ export function MetricTargetSummary({ slug }: MetricTargetSummaryProps) {
const { t } = useTranslations();
const { isAuthenticated } = useAuth();
+ const isGlucose = slug === "blood-glucose";
const targetType = SLUG_TO_TARGET_TYPE[slug];
const { data } = useQuery({
@@ -100,23 +136,98 @@ export function MetricTargetSummary({ slug }: MetricTargetSummaryProps) {
const json = await res.json();
return json.data as TargetsResponse;
},
- enabled: isAuthenticated && targetType != null,
+ enabled: isAuthenticated && (targetType != null || isGlucose),
});
// No numeric target for this slug, or the payload hasn't resolved yet.
- if (!targetType || !data) return null;
+ if ((!targetType && !isGlucose) || !data) return null;
+
+ // Glucose: one panel per logged context, converting the canonical
+ // mg/dL values to the user's display unit exactly like the /targets
+ // page. The route's per-context labels are i18n keys, so resolve them.
+ if (isGlucose) {
+ const displayUnit = resolveGlucoseUnit(data.profile?.glucoseUnit ?? null);
+ const convert = (v: number | null) =>
+ v == null ? null : convertGlucose(v, displayUnit);
+ const panels = GLUCOSE_TARGET_TYPES.map((type) =>
+ data.targets.find((entry) => entry.type === type),
+ )
+ .filter((entry): entry is TargetItem => entry != null && entry.range != null)
+ .map((entry) => ({
+ ...entry,
+ label: t(entry.label),
+ unit: displayUnit,
+ current: convert(entry.current),
+ average30: convert(entry.average30),
+ range: entry.range
+ ? {
+ min: convertGlucose(entry.range.min, displayUnit),
+ max: convertGlucose(entry.range.max, displayUnit),
+ }
+ : null,
+ }));
+
+ if (panels.length === 0) return null;
+
+ return (
+
+ {panels.map((panel) => (
+
+ ))}
+
+ );
+ }
const target = data.targets.find((entry) => entry.type === targetType);
if (!target || !target.range) return null;
+ return (
+
+ );
+}
+
+interface TargetReferencePanelProps {
+ target: TargetItem;
+ bpDiastolic?: TargetsResponse["bpDiastolic"];
+ /** Optional sub-heading shown above the panel (used for glucose contexts). */
+ heading?: string;
+}
+
+/**
+ * One compact reference panel: range + status pill + source, a range
+ * bar (plus a diastolic bar for BP), the in-target share + 30-day
+ * average, the 7-day consistency strip, and the adjust-target link.
+ * All fields come from the `/api/insights/targets` payload — nothing is
+ * recomputed here.
+ */
+function TargetReferencePanel({
+ target,
+ bpDiastolic,
+ heading,
+}: TargetReferencePanelProps) {
+ const { t } = useTranslations();
+
const { range, unit } = target;
+ if (!range) return null;
+
+ const isBp = target.type === "BLOOD_PRESSURE";
+ const isMedicationCompliance = target.type === "MEDICATION_COMPLIANCE";
// Compose the range string. Blood pressure stitches the systolic band
// (on this target) with the diastolic band (on `bpDiastolic`) so the
// user reads the familiar `S/D` pair.
let rangeLabel: string;
- if (targetType === "BLOOD_PRESSURE" && data.bpDiastolic.range) {
- const dia = data.bpDiastolic.range;
+ if (isBp && bpDiastolic?.range) {
+ const dia = bpDiastolic.range;
rangeLabel = t("insights.subPage.target.bpRange", {
sysMin: fmt(range.min),
sysMax: fmt(range.max),
@@ -132,49 +243,138 @@ export function MetricTargetSummary({ slug }: MetricTargetSummaryProps) {
});
}
+ const sourceLink = getTargetSourceLink(target);
+
// In-target share: fraction of logged days in the green band over the
// last 30. Suppressed when the route flagged insufficient data so the
// sub-page mirrors the `/targets` strip exactly.
- const showShare =
- !target.insufficientData && target.daysLogged30d > 0;
+ const showShare = !target.insufficientData && target.daysLogged30d > 0;
const sharePct = showShare
? Math.round((target.daysInRange30d / target.daysLogged30d) * 100)
: null;
+ // 30-day average. BP stitches the diastolic average into the familiar
+ // `S/D` pair; everything else rounds to one decimal.
+ let averageLabel: string | null = null;
+ if (target.average30 != null) {
+ const avgValue =
+ isBp && bpDiastolic?.average30 != null
+ ? `${Math.round(target.average30)}/${Math.round(bpDiastolic.average30)}`
+ : String(Math.round(target.average30 * 10) / 10);
+ averageLabel = `${t("targets.average30d")} ${avgValue} ${unit}`;
+ }
+
+ const showConsistency =
+ !target.insufficientData &&
+ target.consistency7d != null &&
+ target.consistency7d.length > 0;
+
return (
- // v1.8.5 W4a — demote the target summary from a full bordered `Card`
- // (which carried the primitive's `py-4 md:py-6` shell padding around a
- // `py-3` content block — far too much vertical reserve for one line of
- // range text) to a compact inline band. The green target glyph, the
- // range string, the in-target share, and the "adjust" link all stay;
- // only the wrapper shell changes so the block reads as a strip, not a
- // card, in the tightened sub-page rhythm.