diff --git a/CHANGELOG.md b/CHANGELOG.md index edbdd3468..e1facfd3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ ## [Unreleased] +## [1.27.3] — 2026-07-05 — Briefing resilience and Insights readability + +### Fixed + +- The daily briefing no longer disappears when the AI provider fails mid-generation. If the number-grounding recheck cannot run because the provider is unreachable, the previous day's briefing stays in place until the next successful run — a briefing that restates unverified numbers is still withheld, as before. +- Assessments show their full text right away; the three-line cut with the "show more" step is gone. + +### Changed + +- Insights read better: the assessment, the daily briefing, the page explainers, and the "usual range" note now use the regular text colour instead of grey — grey stays reserved for meta lines such as timestamps. +- The "usual range" note is a proper card now, with the same icon-and-title header as every other insights card. +- The page heading and explainer on each metric page line up with the card text below them instead of hanging to the left of it. +- The health-score card drops its "Ask the Coach" corner button. + ## [1.27.2] — 2026-07-04 — Readable AI texts ### Changed diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 22686727a..57bbe3a11 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: HealthLog API - version: 1.27.2 + version: 1.27.3 description: >- Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest. diff --git a/messages/de.json b/messages/de.json index edc5f2450..28604c9b1 100644 --- a/messages/de.json +++ b/messages/de.json @@ -2353,7 +2353,7 @@ "tokensUsedWithModel": "{count} Tokens · {model}", "askAboutThis": "Den Coach dazu fragen", "readStrip": { - "label": "Coach-Einschätzung", + "label": "Üblicher Bereich", "baselineWithin": "Dein üblicher Bereich liegt bei {low}–{high} {unit}; heute liegt {value} darin.", "baselineAbove": "Dein üblicher Bereich liegt bei {low}–{high} {unit}; heute liegt {value} darüber.", "baselineBelow": "Dein üblicher Bereich liegt bei {low}–{high} {unit}; heute liegt {value} darunter.", diff --git a/messages/en.json b/messages/en.json index 58ab35eeb..df45fbd25 100644 --- a/messages/en.json +++ b/messages/en.json @@ -2353,7 +2353,7 @@ "tokensUsedWithModel": "{count} tokens · {model}", "askAboutThis": "Ask the coach about this", "readStrip": { - "label": "Coach read", + "label": "Usual range", "baselineWithin": "Your usual range is {low}–{high} {unit}; today's {value} sits within it.", "baselineAbove": "Your usual range is {low}–{high} {unit}; today's {value} sits above it.", "baselineBelow": "Your usual range is {low}–{high} {unit}; today's {value} sits below it.", diff --git a/messages/es.json b/messages/es.json index eca0a43ce..35f95049a 100644 --- a/messages/es.json +++ b/messages/es.json @@ -2353,7 +2353,7 @@ "tokensUsedWithModel": "{count} tokens · {model}", "askAboutThis": "Pregúntale al coach sobre esto", "readStrip": { - "label": "Lectura del Coach", + "label": "Rango habitual", "baselineWithin": "Tu rango habitual es {low}–{high} {unit}; hoy {value} se sitúa dentro de él.", "baselineAbove": "Tu rango habitual es {low}–{high} {unit}; hoy {value} se sitúa por encima.", "baselineBelow": "Tu rango habitual es {low}–{high} {unit}; hoy {value} se sitúa por debajo.", diff --git a/messages/fr.json b/messages/fr.json index f3dc0a915..92682f7a4 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -2353,7 +2353,7 @@ "tokensUsedWithModel": "{count} jetons · {model}", "askAboutThis": "Interroger le coach à ce sujet", "readStrip": { - "label": "Lecture du Coach", + "label": "Plage habituelle", "baselineWithin": "Ta plage habituelle est de {low}–{high} {unit} ; aujourd'hui, {value} s'y situe.", "baselineAbove": "Ta plage habituelle est de {low}–{high} {unit} ; aujourd'hui, {value} se situe au-dessus.", "baselineBelow": "Ta plage habituelle est de {low}–{high} {unit} ; aujourd'hui, {value} se situe en dessous.", diff --git a/messages/it.json b/messages/it.json index bd725b9e4..6a20cc358 100644 --- a/messages/it.json +++ b/messages/it.json @@ -2353,7 +2353,7 @@ "tokensUsedWithModel": "{count} token · {model}", "askAboutThis": "Chiedi al coach", "readStrip": { - "label": "Lettura del Coach", + "label": "Intervallo abituale", "baselineWithin": "Il tuo intervallo abituale è {low}–{high} {unit}; oggi {value} rientra al suo interno.", "baselineAbove": "Il tuo intervallo abituale è {low}–{high} {unit}; oggi {value} si colloca al di sopra.", "baselineBelow": "Il tuo intervallo abituale è {low}–{high} {unit}; oggi {value} si colloca al di sotto.", diff --git a/messages/pl.json b/messages/pl.json index 45153ba77..179663f05 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -2353,7 +2353,7 @@ "tokensUsedWithModel": "{count} tokenów · {model}", "askAboutThis": "Zapytaj trenera o to", "readStrip": { - "label": "Ocena Coacha", + "label": "Typowy zakres", "baselineWithin": "Twój zwykły zakres to {low}–{high} {unit}; dzisiejsza wartość {value} mieści się w nim.", "baselineAbove": "Twój zwykły zakres to {low}–{high} {unit}; dzisiejsza wartość {value} jest powyżej niego.", "baselineBelow": "Twój zwykły zakres to {low}–{high} {unit}; dzisiejsza wartość {value} jest poniżej niego.", diff --git a/package.json b/package.json index f28045086..1493bd625 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "healthlog", - "version": "1.27.2", + "version": "1.27.3", "description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.", "license": "PolyForm-Noncommercial-1.0.0", "homepage": "https://healthlog.dev", diff --git a/src/components/insights/derived/coach-read-strip.tsx b/src/components/insights/derived/coach-read-strip.tsx index 2d89ca7db..6e5eb68aa 100644 --- a/src/components/insights/derived/coach-read-strip.tsx +++ b/src/components/insights/derived/coach-read-strip.tsx @@ -8,7 +8,7 @@ import { useMounted } from "@/hooks/use-mounted"; import { useTranslations } from "@/lib/i18n/context"; import { queryKeys } from "@/lib/query-keys"; import { apiGet } from "@/lib/api/api-fetch"; -import { cn } from "@/lib/utils"; +import { TileHeader } from "@/components/insights/tile-header"; import type { CoachReadStripData } from "@/lib/insights/derived/coach-read-shape"; /** @@ -109,37 +109,27 @@ export function CoachReadStrip({ // fires defensively.) if (!baselineLine && !driverLine) return null; + // Standard card anatomy — the same TileHeader-led shape as every other + // insights card (foreground icon + title), instead of the former inline + // mini-icon strip that read as chrome outside the card system. Body prose + // in the regular foreground; muted stays reserved for meta lines. return (
- {baselineLine} -
- ) : null} - {driverLine ? ( -- {driverLine} -
- ) : null} -+ {baselineLine} +
+ ) : null} + {driverLine ? ( ++ {driverLine} +
+ ) : null}` to the BLOCK CONTAINER: a max-height + overflow-hidden when collapsed, - // measured on the wrapper node so multi-paragraph text doesn't re-collapse. - // When the text fits, `scrollHeight` equals `clientHeight`; an overflow means - // the clamp is hiding content and the toggle earns its place. Gated on - // `!expanded` so the comparison reads against the clamped element. - useEffect(() => { - if (expanded) return; - const node = wrapperRef.current; - if (!node) return; - - const measure = () => { - setOverflows(node.scrollHeight > node.clientHeight + 1); - }; - measure(); - - if (typeof ResizeObserver === "undefined") return; - const observer = new ResizeObserver(measure); - observer.observe(node); - return () => observer.disconnect(); - // Re-measure whenever the source text changes (a fresh assessment) or - // the user collapses the card again. - }, [text, expanded]); - + // The assessment shows in full — the 3-line clamp + "show more" toggle + // hid the prose behind an extra tap for no gain on a card whose whole + // point is the text. Body prose reads in the regular foreground; muted + // stays reserved for meta lines (timestamps, captions). return ( -
{t(`insights.subPage.explainer.${explainerMetric}Body`)}
diff --git a/src/lib/insights/comprehensive-generate.ts b/src/lib/insights/comprehensive-generate.ts index be4ab8ca4..cef38114d 100644 --- a/src/lib/insights/comprehensive-generate.ts +++ b/src/lib/insights/comprehensive-generate.ts @@ -1111,6 +1111,7 @@ export async function generateComprehensiveInsight( // the briefing rather than persist a fabricated number. { const signals = features.signalsOfDay ?? null; + let retryTransportFailed = false; let ungrounded = findUngroundedBriefingNumbers( readBriefingBlock(insights), signals, @@ -1151,14 +1152,39 @@ export async function generateComprehensiveInsight( ungrounded = retryUngrounded; } } catch { - // Retry failed — fall through to the strip below. + // Retry failed on TRANSPORT (provider outage / timeout), not on + // content — the model never got its correction chance. + retryTransportFailed = true; } } if (ungrounded.length > 0 && insights && typeof insights === "object") { - (insights as Record