fix(insights): title the period-in-review card and hide the day-signal card when there is no signal

Make "Period in review" the period-narrative card heading (a proper
title, not a faint uppercase label) and drop the possessive across all
locales.

Render the today-signal card only when there is an actual signal: an
all-clear day (every vital inside its personal range) or an account still
building its baselines now produces no card, rather than an empty header.
This commit is contained in:
Marc-André Bombeck committed 2026-06-06 23:25:41 +02:00
1 parent 6d5145bdd9
commit 19af59ebfe
9 files changed
+34 -78

No files matched your search

@@ -115,18 +115,14 @@ describe("<CoincidentDeviationCard>", () => {
expect(fired).toContain("min-h-48");
});
it("renders the insufficient (building baselines) state, never an alarm", () => {
it("renders nothing in the insufficient (building baselines) state", () => {
mock(insufficient());
const html = render(<CoincidentDeviationCard />);
expect(html).toContain('data-state="insufficient"');
expect(html).toContain('data-slot="coincident-building"');
expect(html).toContain('data-slot="coverage-meter"');
// Calm, not amber/red.
expect(html).not.toContain("border-warning");
expect(html).not.toContain("text-destructive");
// No signal yet → the section stays absent rather than an empty husk.
expect(html).toBe("");
});
it("renders the all-clear state with the count checked", () => {
it("renders nothing in the all-clear state (no signal to show)", () => {
mock(
ok({
fired: false,
@@ -136,12 +132,8 @@ describe("<CoincidentDeviationCard>", () => {
}),
);
const html = render(<CoincidentDeviationCard />);
expect(html).toContain('data-state="all-clear"');
expect(html).toContain("All your vitals are within their personal range");
expect(html).toContain("text-success");
// No alarm tone.
expect(html).not.toContain("border-warning");
expect(html).not.toContain("text-destructive");
// Every vital inside its personal range → hide the whole card.
expect(html).toBe("");
});
it("renders the watch state for a single out-of-band vital, not the fired tone", () => {
@@ -1,6 +1,6 @@
"use client";
import { CheckCircle2, AlertTriangle, Activity } from "lucide-react";
import { AlertTriangle, Activity } from "lucide-react";
import { useTranslations } from "@/lib/i18n/context";
import { cn } from "@/lib/utils";
@@ -17,22 +17,20 @@ import type { DerivedProvenance } from "@/lib/insights/derived/types";
/**
* v1.10.3 — "Today's signal" headline card.
*
* Promotes the COINCIDENT_DEVIATION flag from one buried below-the-fold vitals
* tile (painted only when it fired) to a dedicated, always-mounted card at the
* top of the Insights overview, matching the always-present pattern Apple
* Vitals / WHOOP Health Monitor / Oura Symptom Radar use. The signal is the
* daily headline read, not an alarm: an "all clear" day is itself the
* reassuring product.
* Surfaces the COINCIDENT_DEVIATION flag at the top of the Insights overview as
* a calm awareness read drawn off the SINGLE existing `Derived` payload — no new
* engine math, no new route, no schema change.
*
* It renders four calm states off the SINGLE existing `Derived` payload — no
* new engine math, no new route, no schema change:
* - insufficient (< 2 banded vitals) → "building your baselines" + coverage.
* - all-clear (ok, !fired, 0 contributing) → a green check + the count
* checked.
* - watch (ok, !fired, 1 contributing) → a calm, non-alert line naming the
* one vital.
* - fired (ok, fired, ≥ 2 contributing) → an amber awareness card naming the
* vitals + the load-bearing "possible factors — never a cause" line.
* The card renders ONLY when there is an actual signal to show. A day where
* every vital sits inside its personal range (all-clear), or an account still
* building its baselines (insufficient), produces no card at all — the section
* stays absent rather than surfacing an empty header. The two states that DO
* render:
* - watch (ok, 1 contributing vital, or a thin-history multi-signal flag) →
* a calm, non-alert line naming the vital.
* - fired (ok, fired, ≥ 2 contributing, with enough history) → an amber
* awareness card naming the vitals + the load-bearing "possible factors —
* never a cause" line.
*
* Restraint guarantees: at most the `warning` (amber) band — never
* `destructive`/red, never a score, never a chart (so no Recharts), never a
@@ -167,22 +165,10 @@ export function CoincidentDeviationCard({
);
}
// Building the baselines — fewer than two banded vitals. Calm, never an
// alarm, never blank.
// Building the baselines — fewer than two banded vitals. There is no signal
// to show, so the section stays absent rather than rendering an empty husk.
if (data.status === "insufficient") {
return (
<div className={className}>
<CardShell state="insufficient" provenance={data.provenance}>
<p
className="text-muted-foreground text-sm"
data-slot="coincident-building"
>
{t("insights.derived.coincident.building")}
</p>
<CoverageMeter coverage={data.coverage} />
</CardShell>
</div>
);
return null;
}
const v = data.value!;
@@ -208,32 +194,10 @@ export function CoincidentDeviationCard({
? "fired"
: "watch";
// All clear — every vital is inside its personal range, so there is no signal
// worth a card. Hide the whole section rather than surface an empty header.
if (state === "all-clear") {
return (
<div className={className}>
<CardShell state="all-clear" provenance={data.provenance}>
<div className="flex items-start gap-2">
<CheckCircle2
className="text-success mt-0.5 h-4 w-4 shrink-0"
aria-hidden="true"
/>
<div className="min-w-0 space-y-1">
<p
className="text-foreground text-sm font-medium"
data-slot="coincident-headline"
>
{t("insights.derived.coincident.allClear")}
</p>
<p className="text-muted-foreground text-xs leading-snug">
{t("insights.derived.coincident.allClearMeta", {
count: v.vitals.length,
})}
</p>
</div>
</div>
</CardShell>
</div>
);
return null;
}
if (state === "watch") {
@@ -159,9 +159,9 @@ export function PeriodNarrativeCard({
className={cn(SHELL, className)}
>
<div className="flex items-center justify-between gap-2">
<span className="text-muted-foreground truncate text-xs font-medium tracking-wide uppercase">
<h2 className="text-foreground truncate text-base font-semibold">
{t("insights.narrativeTitle")}
</span>
</h2>
{narrative?.provenance ? (
<ProvenanceDisclosure provenance={narrative.provenance} />
) : null}