From 28a829695d14ac64db39986b806566ecaf86e9dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 17 May 2026 12:19:43 +0200 Subject: [PATCH] fix(insights): tri-state render contract for trend annotations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The trends-row annotation slot painted "Mehr Daten nötig, um diesen Trend zu kommentieren" whenever the advisor query was in flight or a regenerate was firing — even when the model was about to deliver fresh prose. Two cold-mount conditions collapsed into the same empty hint: advisor pending, and advisor returned null. `` now reads a tri-state `status` prop: - `pending` — advisor query in flight / regenerate firing. Renders a 3-line shimmer block. - `needs_data` — advisor returned `annotation = null`. Renders the "Mehr Daten nötig" hint. - `generated` — advisor returned a string. Renders the prose + optional confidence chip. When `status` is omitted the legacy `annotation == null → empty` mapping still applies (back-compat for existing callers). `` derives the status from a new `loading` prop and the per-metric annotation presence, with pending winning over needs_data so a mid-regenerate cycle never flashes the empty hint between shimmer and new prose. The Insights mother page now passes `loading={advisor.isLoading || advisor.isRegenerating}`. Added unit tests pin the three render-state branches plus the back-compat fallback. New `trendAnnotation.pendingLabel` i18n key across DE / EN / FR / ES / IT / PL drives the aria-label on the pending block. Health Score paint lag is a parent-level analytics query issue and stays for v1.4.37. --- messages/de.json | 3 +- messages/en.json | 3 +- messages/es.json | 3 +- messages/fr.json | 3 +- messages/it.json | 3 +- messages/pl.json | 3 +- src/app/insights/page.tsx | 5 +- .../__tests__/trend-annotation.test.tsx | 84 +++++++++++++++++++ .../insights/__tests__/trends-row.test.tsx | 31 +++++++ src/components/insights/trend-annotation.tsx | 57 ++++++++++++- src/components/insights/trends-row.tsx | 26 +++++- 11 files changed, 211 insertions(+), 10 deletions(-) diff --git a/messages/de.json b/messages/de.json index c58deb2ba..8d9fdb73c 100644 --- a/messages/de.json +++ b/messages/de.json @@ -1004,7 +1004,8 @@ "emptyMood": "Mehr Daten nötig, um diesen Trend zu kommentieren.", "confidenceHigh": "Hohe Konfidenz", "confidenceModerate": "Mittlere Konfidenz", - "confidenceLow": "Geringe Konfidenz" + "confidenceLow": "Geringe Konfidenz", + "pendingLabel": "Trend-Kommentar wird generiert" }, "correlationRow": { "title": "Zusammenhänge", diff --git a/messages/en.json b/messages/en.json index 1e88b791f..01c5c986a 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1004,7 +1004,8 @@ "emptyMood": "Awaiting more data to annotate this trend.", "confidenceHigh": "High confidence", "confidenceModerate": "Moderate confidence", - "confidenceLow": "Low confidence" + "confidenceLow": "Low confidence", + "pendingLabel": "Generating trend commentary" }, "correlationRow": { "title": "Relationships", diff --git a/messages/es.json b/messages/es.json index 5b0eed9bd..283770bdf 100644 --- a/messages/es.json +++ b/messages/es.json @@ -1004,7 +1004,8 @@ "emptyMood": "Awaiting more data to annotate this trend.", "confidenceHigh": "High confidence", "confidenceModerate": "Moderate confidence", - "confidenceLow": "Low confidence" + "confidenceLow": "Low confidence", + "pendingLabel": "Generating trend commentary" }, "correlationRow": { "title": "Relationships", diff --git a/messages/fr.json b/messages/fr.json index 53b130e73..5a2659538 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -1004,7 +1004,8 @@ "emptyMood": "Awaiting more data to annotate this trend.", "confidenceHigh": "High confidence", "confidenceModerate": "Moderate confidence", - "confidenceLow": "Low confidence" + "confidenceLow": "Low confidence", + "pendingLabel": "Generating trend commentary" }, "correlationRow": { "title": "Relationships", diff --git a/messages/it.json b/messages/it.json index 3b151b19f..fa8ad5eb2 100644 --- a/messages/it.json +++ b/messages/it.json @@ -1004,7 +1004,8 @@ "emptyMood": "Awaiting more data to annotate this trend.", "confidenceHigh": "High confidence", "confidenceModerate": "Moderate confidence", - "confidenceLow": "Low confidence" + "confidenceLow": "Low confidence", + "pendingLabel": "Generating trend commentary" }, "correlationRow": { "title": "Relationships", diff --git a/messages/pl.json b/messages/pl.json index 6767bc963..600de05ce 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -1004,7 +1004,8 @@ "emptyMood": "Awaiting more data to annotate this trend.", "confidenceHigh": "High confidence", "confidenceModerate": "Moderate confidence", - "confidenceLow": "Low confidence" + "confidenceLow": "Low confidence", + "pendingLabel": "Generating trend commentary" }, "correlationRow": { "title": "Relationships", diff --git a/src/app/insights/page.tsx b/src/app/insights/page.tsx index 0cfc94edb..cd6514e93 100644 --- a/src/app/insights/page.tsx +++ b/src/app/insights/page.tsx @@ -246,7 +246,10 @@ export default function InsightsPage() { )} - + ); } diff --git a/src/components/insights/__tests__/trend-annotation.test.tsx b/src/components/insights/__tests__/trend-annotation.test.tsx index fdbf5d0be..ac52fbf95 100644 --- a/src/components/insights/__tests__/trend-annotation.test.tsx +++ b/src/components/insights/__tests__/trend-annotation.test.tsx @@ -116,4 +116,88 @@ describe("", () => { /]*data-slot="trend-annotation-empty"[^>]*class="[^"]*line-clamp-3[^"]*"[^>]*>/, ); }); + + // ── v1.4.36 W2 T3 — render-state contract ────────────────────────── + // Pre-fix the component derived "empty" purely from + // `annotation == null`, which painted "Mehr Daten nötig" on every + // cold mount and every regenerate-in-flight even though the advisor + // was about to deliver an annotation. The tri-state `status` prop + // distinguishes pending / needs_data / generated. + + it("status='pending' renders a shimmer block (not the empty hint)", () => { + const html = render( + , + ); + expect(html).toMatch(/data-slot="trend-annotation-pending"/); + expect(html).toContain("animate-pulse"); + // Empty hint and filled prose MUST be absent during pending. + expect(html).not.toContain('data-slot="trend-annotation-empty"'); + expect(html).not.toContain('data-slot="trend-annotation"'); + expect(html).not.toContain("Awaiting more data"); + }); + + it("status='pending' announces a busy state for assistive tech", () => { + const html = render( + , + ); + expect(html).toMatch(/role="status"/); + expect(html).toMatch(/aria-busy="true"/); + }); + + it("status='pending' keeps painting the shimmer even when an annotation is supplied (regenerate-in-flight)", () => { + // During a forced regenerate the previous annotation is still in + // cache. We surface the pending state so the user doesn't read + // stale prose alongside a freshly-fired generation. + const html = render( + , + ); + expect(html).toMatch(/data-slot="trend-annotation-pending"/); + expect(html).not.toContain("stale annotation"); + }); + + it("status='needs_data' renders the empty hint regardless of annotation presence", () => { + const html = render( + , + ); + expect(html).toMatch(/data-slot="trend-annotation-empty"/); + expect(html).toContain("Awaiting more data"); + expect(html).not.toContain("this should be ignored"); + }); + + it("status='generated' renders the prose + confidence chip", () => { + const html = render( + , + ); + expect(html).toMatch(/data-slot="trend-annotation"/); + expect(html).toContain("BP trending down 4 mmHg"); + expect(html).toMatch(/data-slot="trend-annotation-confidence"/); + expect(html).not.toContain("Awaiting more data"); + }); + + it("omitting status falls back to the legacy annotation==null mapping (back-compat)", () => { + // Old call sites that don't pass `status` must still see the + // previous two-state behaviour: filled prose when supplied, empty + // hint when null. + const filled = render( + , + ); + expect(filled).toMatch(/data-slot="trend-annotation"/); + expect(filled).toContain("BP trending down"); + + const empty = render(); + expect(empty).toMatch(/data-slot="trend-annotation-empty"/); + }); }); diff --git a/src/components/insights/__tests__/trends-row.test.tsx b/src/components/insights/__tests__/trends-row.test.tsx index e3ccf086b..f5ed5daf1 100644 --- a/src/components/insights/__tests__/trends-row.test.tsx +++ b/src/components/insights/__tests__/trends-row.test.tsx @@ -128,6 +128,37 @@ describe("", () => { expect(matches.length).toBe(3); }); + it("loading=true paints the pending shimmer for every metric (no 'mehr Daten' flash)", () => { + // v1.4.36 W2 T3 — pre-fix the row painted "Awaiting more data" + // across all three metrics whenever the advisor query was in + // flight. The loading flag now propagates to each annotation + // slot so the user reads a generating-state shimmer instead. + const html = render(); + const pending = + html.match(/data-slot="trend-annotation-pending"/g) ?? []; + expect(pending.length).toBe(3); + expect(html).not.toContain("Awaiting more data"); + }); + + it("loading=true keeps the shimmer even when annotations are supplied (regenerate-in-flight)", () => { + const html = render( + , + ); + expect(html).not.toContain("stale BP"); + expect(html).not.toContain("stale weight"); + expect(html).not.toContain("stale mood"); + const pending = + html.match(/data-slot="trend-annotation-pending"/g) ?? []; + expect(pending.length).toBe(3); + }); + it("propagates per-metric confidence chips", () => { const html = render( = { @@ -48,10 +75,36 @@ export function TrendAnnotation({ metric, annotation, confidence, + status, }: TrendAnnotationProps) { const { t } = useTranslations(); - if (!annotation) { + // v1.4.36 W2 T3 — render-state contract. When `status` is supplied + // it drives the branch directly; otherwise we fall back on the + // legacy `annotation == null → empty` mapping so existing callers + // (tests, isolated mounts) keep their previous behaviour. + const resolvedStatus: TrendAnnotationStatus = + status ?? (annotation ? "generated" : "needs_data"); + + if (resolvedStatus === "pending") { + return ( +
+
+
+
+
+ ); + } + + if (resolvedStatus === "needs_data" || !annotation) { return (

{ + if (loading) return "pending"; + return annotation ? "generated" : "needs_data"; + }; return (