mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
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:
1 parent
6d5145bdd9
commit
19af59ebfe
9 files changed
+34
-78
No files matched your search
+1
-1
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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}
|
||||
|
||||
Reference in new issue
Block a user