From 789ec60d97816652a1cbafca4fbc7a8abfade523 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 3 Jun 2026 19:22:52 +0200 Subject: [PATCH 01/15] feat(insights): elevate coincident-deviation to a dedicated "Today's signal" card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Promote the COINCIDENT_DEVIATION flag from a fire-only tile buried in the below-the-fold vitals grid to an always-mounted headline card at the top of the Insights overview. The card renders four calm states off the single existing derived payload — building-baselines, all-clear, watch, fired — with no new engine math, route, or schema change. Keep it calm and clinically safe: at most an amber band, never red, no score, no chart, no push. Soften a fired flag to the watch tone when the backing history is thin. The fired/watch states carry the load-bearing "possible factors, never a cause / not a diagnosis" framing and reuse ProvenanceExplainer + CoverageMeter + the band tokens verbatim. Remove the now-redundant tile, its batch token, and the hasRenderableVital branch from the vitals grid so the signal is not shown twice. --- messages/de.json | 10 +- messages/en.json | 10 +- messages/es.json | 10 +- messages/fr.json | 10 +- messages/it.json | 10 +- messages/pl.json | 10 +- src/app/insights/page.tsx | 20 ++ .../coincident-deviation-card.test.tsx | 194 ++++++++++++ .../insights/coincident-deviation-card.tsx | 284 ++++++++++++++++++ .../__tests__/vitals-dashboard.test.tsx | 32 +- .../insights/derived/vitals-dashboard.tsx | 74 +---- 11 files changed, 562 insertions(+), 102 deletions(-) create mode 100644 src/components/insights/__tests__/coincident-deviation-card.test.tsx create mode 100644 src/components/insights/coincident-deviation-card.tsx diff --git a/messages/de.json b/messages/de.json index 10928eb9..bc196e44 100644 --- a/messages/de.json +++ b/messages/de.json @@ -2632,7 +2632,15 @@ "coincident": { "label": "Mehrere Vitalwerte außerhalb der Baseline", "summary": "{count} deiner Vitalwerte lagen heute außerhalb ihres persönlichen Bereichs.", - "vitals": "Außerhalb des Bereichs: {list}" + "vitals": "Außerhalb des Bereichs: {list}", + "cardTitle": "Signal des Tages", + "allClear": "Alle deine Vitalwerte liegen heute in ihrem persönlichen Bereich.", + "allClearMeta": "{count} deiner Vitalwerte gegen deinen eigenen typischen Bereich geprüft.", + "watch": "Ein Vitalwert liegt heute außerhalb seines üblichen Bereichs.", + "watchVital": "Außerhalb des Bereichs: {vital}. Oft nur einmalig — behalte es in den nächsten Tagen im Blick.", + "firedHeadline": "{count} deiner Vitalwerte liegen heute außerhalb ihres persönlichen Bereichs.", + "factors": "Mögliche Faktoren — nie eine Ursache: ein hartes Training, schlechter Schlaf, Alkohol, Höhe, Stress oder Krankheit. Das ist ein Hinweis aus deinen eigenen Baselines, keine Diagnose.", + "building": "Deine persönlichen Baselines entstehen — erfasse ein paar Tage mehr, dann erscheint dies." } }, "cardioFitness": { diff --git a/messages/en.json b/messages/en.json index 84e8cfc5..441fb7fd 100644 --- a/messages/en.json +++ b/messages/en.json @@ -2632,7 +2632,15 @@ "coincident": { "label": "Several vitals off baseline", "summary": "{count} of your vitals sat outside their personal range today.", - "vitals": "Outside range: {list}" + "vitals": "Outside range: {list}", + "cardTitle": "Today's signal", + "allClear": "All your vitals are within their personal range today.", + "allClearMeta": "Checked {count} of your vitals against your own typical range.", + "watch": "One vital is outside its usual range today.", + "watchVital": "Outside range: {vital}. Often just a one-off — worth a glance over the next few days.", + "firedHeadline": "{count} of your vitals are outside their personal range today.", + "factors": "Possible factors — never a cause: a hard workout, poor sleep, alcohol, altitude, stress, or illness. This is awareness from your own baselines, not a diagnosis.", + "building": "Building your personal baselines — track a few more days and this will appear." } }, "cardioFitness": { diff --git a/messages/es.json b/messages/es.json index e6741d04..a8a352af 100644 --- a/messages/es.json +++ b/messages/es.json @@ -2632,7 +2632,15 @@ "coincident": { "label": "Varias constantes fuera de la base", "summary": "Hoy, {count} de tus constantes quedaron fuera de su rango personal.", - "vitals": "Fuera de rango: {list}" + "vitals": "Fuera de rango: {list}", + "cardTitle": "Señal de hoy", + "allClear": "Hoy todas tus constantes están dentro de su rango personal.", + "allClearMeta": "Se revisaron {count} de tus constantes frente a tu propio rango habitual.", + "watch": "Hoy una constante está fuera de su rango habitual.", + "watchVital": "Fuera de rango: {vital}. A menudo es algo puntual; conviene vigilarlo los próximos días.", + "firedHeadline": "Hoy, {count} de tus constantes están fuera de su rango personal.", + "factors": "Posibles factores, nunca una causa: un entrenamiento intenso, dormir mal, alcohol, altitud, estrés o una enfermedad. Es un aviso a partir de tus propias bases, no un diagnóstico.", + "building": "Construyendo tus bases personales: registra unos días más y esto aparecerá." } }, "cardioFitness": { diff --git a/messages/fr.json b/messages/fr.json index 79b4a319..87906de3 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -2632,7 +2632,15 @@ "coincident": { "label": "Plusieurs constantes hors référence", "summary": "Aujourd’hui, {count} de vos constantes sont sorties de leur plage personnelle.", - "vitals": "Hors plage : {list}" + "vitals": "Hors plage : {list}", + "cardTitle": "Signal du jour", + "allClear": "Aujourd’hui, toutes vos constantes sont dans leur plage personnelle.", + "allClearMeta": "{count} de vos constantes vérifiées par rapport à votre propre plage habituelle.", + "watch": "Aujourd’hui, une constante est hors de sa plage habituelle.", + "watchVital": "Hors plage : {vital}. Souvent ponctuel — à surveiller ces prochains jours.", + "firedHeadline": "Aujourd’hui, {count} de vos constantes sont hors de leur plage personnelle.", + "factors": "Facteurs possibles — jamais une cause : un entraînement intense, un mauvais sommeil, l’alcool, l’altitude, le stress ou une maladie. C’est un repère issu de vos propres références, pas un diagnostic.", + "building": "Construction de vos références personnelles — enregistrez quelques jours de plus et cela apparaîtra." } }, "cardioFitness": { diff --git a/messages/it.json b/messages/it.json index be4f3266..5ca6bd9d 100644 --- a/messages/it.json +++ b/messages/it.json @@ -2632,7 +2632,15 @@ "coincident": { "label": "Più parametri fuori dalla base", "summary": "Oggi {count} dei tuoi parametri vitali sono usciti dal loro intervallo personale.", - "vitals": "Fuori intervallo: {list}" + "vitals": "Fuori intervallo: {list}", + "cardTitle": "Segnale di oggi", + "allClear": "Oggi tutti i tuoi parametri vitali sono nel loro intervallo personale.", + "allClearMeta": "Controllati {count} dei tuoi parametri rispetto al tuo intervallo abituale.", + "watch": "Oggi un parametro è fuori dal suo intervallo abituale.", + "watchVital": "Fuori intervallo: {vital}. Spesso è un episodio isolato — tienilo d’occhio nei prossimi giorni.", + "firedHeadline": "Oggi {count} dei tuoi parametri vitali sono fuori dal loro intervallo personale.", + "factors": "Possibili fattori — mai una causa: un allenamento intenso, poco sonno, alcol, altitudine, stress o una malattia. È un avviso basato sui tuoi parametri, non una diagnosi.", + "building": "Stiamo costruendo i tuoi riferimenti personali — registra ancora qualche giorno e comparirà." } }, "cardioFitness": { diff --git a/messages/pl.json b/messages/pl.json index acbf8933..f4201312 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -2632,7 +2632,15 @@ "coincident": { "label": "Kilka parametrów poza bazą", "summary": "Dziś {count} Twoich parametrów życiowych znalazło się poza osobistym zakresem.", - "vitals": "Poza zakresem: {list}" + "vitals": "Poza zakresem: {list}", + "cardTitle": "Sygnał dnia", + "allClear": "Dziś wszystkie Twoje parametry życiowe mieszczą się w osobistym zakresie.", + "allClearMeta": "Sprawdzono {count} Twoich parametrów względem Twojego typowego zakresu.", + "watch": "Dziś jeden parametr jest poza swoim zwykłym zakresem.", + "watchVital": "Poza zakresem: {vital}. Często to jednorazowy odczyt — warto obserwować przez kilka dni.", + "firedHeadline": "Dziś {count} Twoich parametrów życiowych jest poza osobistym zakresem.", + "factors": "Możliwe czynniki — nigdy przyczyna: intensywny trening, słaby sen, alkohol, wysokość, stres lub choroba. To wskazówka z Twoich własnych baz, nie diagnoza.", + "building": "Budujemy Twoje osobiste bazy — zarejestruj jeszcze kilka dni, a to się pojawi." } }, "cardioFitness": { diff --git a/src/app/insights/page.tsx b/src/app/insights/page.tsx index 1302c484..df65d17c 100644 --- a/src/app/insights/page.tsx +++ b/src/app/insights/page.tsx @@ -97,6 +97,24 @@ const RhythmEventsCard = dynamic( })), { ssr: false }, ); +// v1.10.3 — "Today's signal" headline card. Promotes COINCIDENT_DEVIATION from +// a buried below-the-fold tile to the top-of-overview daily read (the +// always-present Apple/WHOOP/Oura pattern). Deferred behind `next/dynamic`; it +// owns its own derived-metric query and renders four calm states. A +// fixed-height skeleton matches the resolved card so the top of the page does +// not shift while the chunk + data resolve. +const CoincidentDeviationCard = dynamic( + () => + import("@/components/insights/coincident-deviation-card").then((mod) => ({ + default: mod.CoincidentDeviationCard, + })), + { + ssr: false, + loading: () => ( +
+ ), + }, +); /** * v1.4.25 W4d — Insights mother page. @@ -246,6 +264,8 @@ export default function InsightsPage() { healthScore={analytics?.healthScore ?? undefined} /> + +