Say what the AI surfaces actually compute

Three places where the copy promised more than the handler delivers.

The hero's personal-baseline line rendered unconditionally while both its
siblings in the same meta row gate on their own data. It claims the subtitle
above it was drawn from the reader's own last 90 days, but with no briefing the
subtitle is fixed fallback copy computed from nothing — so an account with no
briefing at all was told its generic sentence rested on a personal baseline.
Gate it on the briefing, like the rest of the row.

The pulse sub-page promised a personalised Karvonen target band. Karvonen is
not computed anywhere: the band comes from CDC/NCHS population percentiles by
age and sex, and falls back to a fixed 60-100 for a reader with no age on file.
The copy now says that in all six locales, and the fallback's own source label
says it is a general adult range rather than a personal one, because that arm
is the same band for everyone. Karvonen itself stays unimplemented; whether to
build it is a product decision, not a copy fix.

The Coach's `allTime` window returns 365 days, identical to `lastYear`, and it
is the default. "year so far" was wrong too — a rolling 365 days is not
year-to-date. Both labels now say what the reads do. The two options remain
functionally identical and one of them should probably go, but removing a
default is a product decision.
This commit is contained in:
Marc-André Bombeck committed 2026-08-22 16:14:54 +02:00
1 parent c1811bc85b
commit 6f2f43bb49
10 files changed
+68 -30

No files matched your search

@@ -241,12 +241,35 @@ describe("<HeroStrip>", () => {
);
});
it("renders the personal-baseline meta line", () => {
const html = render(<HeroStrip briefing={null} now={morningLocal} />);
it("renders the personal-baseline meta line under a real briefing", () => {
const html = render(
<HeroStrip briefing={sampleBriefing} now={morningLocal} />,
);
expect(html).toMatch(/data-slot="insights-hero-strip-baseline"/);
expect(html).toContain("Based on your last 90 days");
});
/**
* Inverted from the previous assertion, which rendered with
* `briefing={null}` and still expected the baseline line.
*
* That pinned the defect. The line claims the subtitle above it was drawn
* from the reader's own last 90 days, but with no briefing the subtitle is
* `heroFallbackSubtitle` — fixed copy computed from nothing. Both siblings in
* the same meta row (the freshness caption, the no-provider hint) already
* gate on their own data; this one did not, so an account with no briefing at
* all was told its generic sentence rested on a personal baseline.
*/
it("withholds the personal-baseline meta line when there is no briefing", () => {
const html = render(<HeroStrip briefing={null} now={morningLocal} />);
expect(html).not.toMatch(/data-slot="insights-hero-strip-baseline"/);
expect(html).not.toContain("Based on your last 90 days");
// The fallback subtitle still renders — only the provenance claim goes.
expect(html).toContain(
"A daily read of your trends, drawn straight from the numbers you&#x27;ve logged.",
);
});
it("renders the freshness caption when updatedAt is supplied", () => {
// v1.22 — the hero freshness line now uses `formatUpdatedLabel` for parity
// with the briefing + per-metric cards: a same-day timestamp reads
+16 -6
View File
@@ -199,14 +199,24 @@ export function HeroStrip({
<ProseBlocks text={subtitle} />
</div>
<div className="text-muted-foreground flex flex-wrap items-center gap-x-3 gap-y-1 text-xs">
<span data-slot="insights-hero-strip-baseline">
{t("insights.heroPersonalBaseline")}
</span>
{/* The baseline line describes where the SUBTITLE came from, so it
may only show when the subtitle IS a briefing. Without one the
subtitle falls back to generic copy derived from nothing, and
this line went on claiming a personal baseline underneath it —
the one part of the row that was ungated while both its
siblings already checked for their own data first. */}
{briefing && (
<span data-slot="insights-hero-strip-baseline">
{t("insights.heroPersonalBaseline")}
</span>
)}
{generatedLine && (
<>
<span aria-hidden="true" className="opacity-50">
·
</span>
{briefing && (
<span aria-hidden="true" className="opacity-50">
·
</span>
)}
<span data-slot="insights-hero-strip-generated">
{generatedLine}
</span>
@@ -28,8 +28,8 @@ import { apiGet } from "@/lib/api/api-fetch";
* nothing and a cold sub-page warms the cache its siblings reuse. 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, the ADA / DDG
* glucose bands, and the medication / mood targets.
* band, the CDC/NCHS percentile pulse band, the AASM sleep band, the
* ADA / DDG glucose bands, and the medication / mood targets.
*
* The panel is a compact reference panel carrying the full target
* context: