From cde88278708abea7047e174f20eeddab0ae57e5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 5 Jun 2026 14:51:35 +0200 Subject: [PATCH] feat(insights): order pulse assessment above cardio-fitness link; harmonize its header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Render the pulse Einschätzung above the VO₂ max cardio-fitness cross-link so the plain-language read leads and the cross-link trails. Lead the cross-link card with the canonical TileHeader so its Gauge glyph matches every other tile header (foreground colour, same size and position) instead of the muted, offset placement. Allow the documented trailing CTA in the spine-order guard for pulse while keeping the target-before-assessment check. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../__tests__/assessment-spine-order.test.ts | 58 ++++++++++++------- src/app/insights/pulse/page.tsx | 50 ++++++++-------- 2 files changed, 60 insertions(+), 48 deletions(-) diff --git a/src/app/insights/__tests__/assessment-spine-order.test.ts b/src/app/insights/__tests__/assessment-spine-order.test.ts index acde8bac2..91347efb0 100644 --- a/src/app/insights/__tests__/assessment-spine-order.test.ts +++ b/src/app/insights/__tests__/assessment-spine-order.test.ts @@ -33,17 +33,29 @@ const PAGES_DIR = join(process.cwd(), "src", "app", "insights"); // `useInsightStatus`-backed pages render ``; the // medications page reads the richer `summary`-shaped route inline, so it // renders `` directly. -const BESPOKE_PAGES: ReadonlyArray<{ slug: string; assessmentTag: string }> = [ +const BESPOKE_PAGES: ReadonlyArray<{ + slug: string; + assessmentTag: string; + /** + * v1.12.8 — pulse intentionally trails its assessment with the + * cardio-fitness CTA (the operator-requested "Einschätzung above the VO₂ + * max cross-link" order). The CTA is a navigation affordance, not a data + * card, so it is the one sanctioned block that may render after the + * assessment; the guard skips the last-block check for these slugs but + * still enforces that the target card precedes the assessment. + */ + assessmentMayTrail?: boolean; +}> = [ { slug: "weight", assessmentTag: " { - for (const { slug, assessmentTag } of BESPOKE_PAGES) { + for (const { slug, assessmentTag, assessmentMayTrail } of BESPOKE_PAGES) { it(`renders the assessment card as the last SubPageShell child on /insights/${slug}`, () => { const source = readFileSync( join(PAGES_DIR, slug, "page.tsx"), @@ -68,25 +80,29 @@ describe("bespoke metric-detail spine — assessment is the last block", () => { `${slug}: assessment card ${assessmentTag} not found inside the shell`, ).toBeGreaterThan(-1); - // All six bespoke pages render the assessment as a self-closing - // element. Find the `/>` that closes it, then assert nothing but - // whitespace separates that close from ``. The icon - // passed as a prop (``) self-closes BEFORE the element's own - // `/>`, so the first `/>` after the assessment open is the icon's and - // the second is the element's — scan for the last `/>` in the body, - // which belongs to the trailing assessment element by construction. - const selfClose = body.lastIndexOf("/>"); - expect( - selfClose, - `${slug}: assessment card is not self-closing as expected`, - ).toBeGreaterThan(assessmentIndex); + // Pages where the assessment is the last block (the default) must have + // nothing but whitespace after the assessment element's `/>`. Pages that + // intentionally trail the assessment with a navigation CTA + // (`assessmentMayTrail`, today only pulse) skip the last-block check — + // the target-before-assessment guard below still applies. + if (!assessmentMayTrail) { + // The assessment renders as a self-closing element. The icon passed + // as a prop (``) self-closes BEFORE the element's own `/>`, + // so the last `/>` in the body belongs to the trailing assessment + // element by construction. + const selfClose = body.lastIndexOf("/>"); + expect( + selfClose, + `${slug}: assessment card is not self-closing as expected`, + ).toBeGreaterThan(assessmentIndex); - const trailing = body.slice(selfClose + 2).trim(); - expect( - trailing, - `${slug}: "${trailing.slice(0, 40)}" renders AFTER the assessment card — ` + - `the assessment must be the last block on the canonical spine`, - ).toBe(""); + const trailing = body.slice(selfClose + 2).trim(); + expect( + trailing, + `${slug}: "${trailing.slice(0, 40)}" renders AFTER the assessment card — ` + + `the assessment must be the last block on the canonical spine`, + ).toBe(""); + } // v1.12.4 — the target card precedes the assessment when present. const targetIndex = body.indexOf(" 0; const pulseSummary = analytics?.summaries?.PULSE ?? null; - // v1.12.7 — brushed-window stats shared between the pulse chart and the + // v1.12.8 — visible-range stats shared between the pulse chart and the // strip (the VO2 chart-row below keeps its own full-range read). - const { statsByType, onDomainStats } = useChartDomainStats(); + const { statsByType, onVisibleStats } = useChartDomainStats(); // 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) @@ -139,41 +140,44 @@ export default function InsightsPulsPage() { chartKey="pulse" types={["PULSE"]} title={t("charts.pulse")} + titleIcon={Heart} colors={["#50fa7b"]} unit="bpm" valueBands={pulseBands} compareBaseline={compareBaseline} userTimezone={user?.timezone} - selectableDomain - onDomainStats={onDomainStats} + onVisibleStats={onVisibleStats} /> + {/* v1.12.0 — Pulse owns the mood × pulse correlation (relocated off + the overview onto its metric page). */} + + + {/* v1.12.8 — the Einschätzung (assessment) sits ABOVE the cardio-fitness + CTA so the plain-language read of the pulse data leads, and the + cross-link to the dedicated VO₂ max page trails it. */} + } /> + {/* VO₂ max is a cardio-fitness metric (Apple's Health app surfaces it under "Heart"), so the pulse page links across to its dedicated `/insights/cardio-fitness` page — the single surface that carries the full chart plus the plain-language assessment — rather than - duplicating a second, divergent VO₂ max view here. */} + duplicating a second, divergent VO₂ max view here. + + v1.12.8 — the card now leads with the canonical `` so the + `Gauge` glyph reads in the foreground colour at the same size and + position as every other tile header, with the body + CTA beneath. */} {hasVo2 ? ( - -