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

+1 -1
View File
@@ -1642,7 +1642,7 @@
"regenerateAnalysis": "Einschätzung neu erstellen",
"regenerateSuccess": "Einschätzung wurde neu erstellt",
"warmStarted": "Einschätzungen werden im Hintergrund erstellt",
"narrativeTitle": "Dein Zeitraum im Rückblick",
"narrativeTitle": "Zeitraum im Rückblick",
"narrativeWeek": "Diese Woche",
"narrativeMonth": "Dieser Monat",
"narrativePreparing": "Deine Zusammenfassung wird erstellt…",
+1 -1
View File
@@ -1642,7 +1642,7 @@
"regenerateAnalysis": "Re-run assessment",
"regenerateSuccess": "Assessment refreshed",
"warmStarted": "Assessments are being prepared in the background",
"narrativeTitle": "Your period in review",
"narrativeTitle": "Period in review",
"narrativeWeek": "This week",
"narrativeMonth": "This month",
"narrativePreparing": "Preparing your summary…",
+1 -1
View File
@@ -1642,7 +1642,7 @@
"regenerateAnalysis": "Volver a generar la valoración",
"regenerateSuccess": "Valoración regenerada",
"warmStarted": "Las valoraciones se están preparando en segundo plano",
"narrativeTitle": "Tu período en resumen",
"narrativeTitle": "Período en resumen",
"narrativeWeek": "Esta semana",
"narrativeMonth": "Este mes",
"narrativePreparing": "Preparando tu resumen…",
+1 -1
View File
@@ -1642,7 +1642,7 @@
"regenerateAnalysis": "Relancer l’évaluation",
"regenerateSuccess": "Évaluation regénérée",
"warmStarted": "Les évaluations sont en cours de préparation en arrière-plan",
"narrativeTitle": "Votre période en revue",
"narrativeTitle": "Période en revue",
"narrativeWeek": "Cette semaine",
"narrativeMonth": "Ce mois-ci",
"narrativePreparing": "Préparation de votre résumé…",
+1 -1
View File
@@ -1642,7 +1642,7 @@
"regenerateAnalysis": "Rigenera la valutazione",
"regenerateSuccess": "Valutazione rigenerata",
"warmStarted": "Le valutazioni vengono preparate in background",
"narrativeTitle": "Il tuo periodo in sintesi",
"narrativeTitle": "Periodo in sintesi",
"narrativeWeek": "Questa settimana",
"narrativeMonth": "Questo mese",
"narrativePreparing": "Preparazione del riepilogo…",
+1 -1
View File
@@ -1642,7 +1642,7 @@
"regenerateAnalysis": "Uruchom ocenę ponownie",
"regenerateSuccess": "Ocena wygenerowana ponownie",
"warmStarted": "Oceny są przygotowywane w tle",
"narrativeTitle": "Twój okres w skrócie",
"narrativeTitle": "Okres w skrócie",
"narrativeWeek": "Ten tydzień",
"narrativeMonth": "Ten miesiąc",
"narrativePreparing": "Przygotowywanie podsumowania…",
@@ -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}