mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 06:52:04 +00:00
fix(insights): handle legacy cached payload without rationale (regenerate CTA)
Cached insight blobs from before the v1.4.16 strict schema landed
crashed /insights with `Cannot read properties of undefined (reading
'replace')` on `stripChartTokens(insight.summary)`. The pre-strict
shape (`{changed, stable, drivers, nextSteps, confidence,
limitations}`) has no `summary`, no `recommendations[]`, and no
`findings[]`, so the rich card had nothing to render and the
chart-token helper crashed on undefined input.
The advisor card now short-circuits to a self-contained legacy-payload
card with a one-click regenerate CTA when the shape is unrenderable.
`isLegacyInsightPayload()` also flags the v1.4.14 pre-strict shape so
the API surfaces the right `legacyPayload: true` flag from a cache
hit. Defense-in-depth: `stripChartTokens()` and `parseChartTokens()`
treat null/undefined as the empty string instead of crashing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
57de2177a9
commit
79bfa27d65
6 files changed
+210
-12
No files matched your search
@@ -267,6 +267,45 @@ describe("<InsightAdvisorCard> — polished loading / empty / error states (B1b)
|
||||
expect(html).toMatch(/data-slot="insight-retry-button"/);
|
||||
});
|
||||
|
||||
it("renders the legacy v1.4.14 payload without crashing (regenerate CTA)", () => {
|
||||
// Reproduces Marc's production /insights crash on 2026-05-10:
|
||||
//
|
||||
// TypeError: Cannot read properties of undefined (reading 'replace')
|
||||
// at stripChartTokens(insight.summary)
|
||||
//
|
||||
// The cached blob in the DB predates v1.4.16's strict insight schema
|
||||
// (no `summary`, no `recommendations[]`, no `findings[]`, no
|
||||
// `dataQuality`, no `disclaimer` — it carries the old `changed`,
|
||||
// `stable`, `drivers`, `nextSteps`, `confidence`, `limitations`
|
||||
// shape). The route's `safeParse(parsed)` fails and falls through
|
||||
// to `insights = parsed`, so the legacy blob reaches the card with
|
||||
// `summary === undefined`. The card must surface a regenerate CTA
|
||||
// and skip rendering the prose, NOT crash.
|
||||
const legacy = {
|
||||
changed: "Long-term improvement on weight and BP.",
|
||||
stable: "Pulse remains stable.",
|
||||
drivers: "Weight reduction may have contributed.",
|
||||
nextSteps: "Keep going.",
|
||||
confidence: "hoch",
|
||||
limitations: "Correlations don't imply causation.",
|
||||
} as unknown as InsightResult;
|
||||
|
||||
const html = renderToStaticMarkup(
|
||||
<I18nProvider initialLocale="en">
|
||||
<InsightAdvisorCard
|
||||
title="Test card"
|
||||
insight={legacy}
|
||||
legacyPayload
|
||||
onRegenerate={() => {}}
|
||||
/>
|
||||
</I18nProvider>,
|
||||
);
|
||||
|
||||
// No crash means the test got here. Surface the regenerate CTA so
|
||||
// the user has a one-click escape from the legacy state.
|
||||
expect(html).toContain('data-slot="insight-legacy-payload-cta"');
|
||||
});
|
||||
|
||||
it("error state without onRegenerate hides the retry button", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<I18nProvider initialLocale="en">
|
||||
|
||||
@@ -432,6 +432,80 @@ export function InsightAdvisorCard({
|
||||
);
|
||||
}
|
||||
|
||||
// ── Legacy-Payload State ──────────────────────────────
|
||||
// v1.4.17 hotfix: when the cached blob predates the v1.4.16 strict
|
||||
// schema (no `summary`, no `recommendations[]`, no `findings[]` —
|
||||
// the v1.4.14 `{changed, stable, drivers, ...}` shape), the rich-card
|
||||
// render path below would crash on `stripChartTokens(undefined)`.
|
||||
// Surface the regenerate CTA in a self-contained card and skip the
|
||||
// rich body entirely. The `legacyPayload` prop is the explicit
|
||||
// signal from the API; we also defensively short-circuit when the
|
||||
// shape doesn't carry the modern `summary` string at all.
|
||||
const isUnrenderable =
|
||||
typeof insight.summary !== "string" ||
|
||||
!Array.isArray(insight.findings) ||
|
||||
!Array.isArray(insight.recommendations);
|
||||
if (legacyPayload || isUnrenderable) {
|
||||
return (
|
||||
<Card data-slot="insight-legacy-card">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{icon ?? <Sparkles className="text-dracula-purple h-5 w-5" />}
|
||||
<CardTitle className="text-lg">
|
||||
{t("insights.aiAnalysisTitle")}
|
||||
</CardTitle>
|
||||
</div>
|
||||
{onRegenerate && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={onRegenerate}
|
||||
disabled={regenerating}
|
||||
title={t("insights.refreshAnalysis")}
|
||||
>
|
||||
{regenerating ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm">{title}</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div
|
||||
className="border-dracula-yellow/30 bg-dracula-yellow/10 flex items-start gap-2 rounded-md border px-3 py-2 text-sm"
|
||||
data-slot="insight-legacy-payload-cta"
|
||||
>
|
||||
<Info className="text-dracula-yellow mt-0.5 h-4 w-4 shrink-0" />
|
||||
<div className="space-y-2">
|
||||
<p>{t("insights.recommendation.legacyPayloadCta")}</p>
|
||||
{onRegenerate && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onRegenerate}
|
||||
disabled={regenerating}
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
{regenerating ? (
|
||||
<Loader2 className="mr-1.5 h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<Sparkles className="mr-1.5 h-3 w-3" />
|
||||
)}
|
||||
{t("insights.startAnalysis")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Full Insight Card ─────────────────────────────────
|
||||
return (
|
||||
<Card className="animate-insight-in">
|
||||
|
||||
Reference in new issue
Block a user