fix(insights): recoverable read failures on the ECG detail and list page

The ECG detail rendered a read failure as a muted, non-recoverable line;
the routed list page had no error branch at all, leaving the shell
heading over an empty body that read the same as loading. The detail now
paints a QueryErrorCard with retry (an alert is content, not muted meta),
and the page shows the same instead of an empty shell. The overview teaser
still unmounts silently, which is right for the overview. Tests watched
each fail first.
This commit is contained in:
Marc-André Bombeck committed 2026-08-09 04:13:46 +02:00
1 parent 22d5c01a68
commit 1229d54d30
4 files changed
+72 -12

No files matched your search

@@ -78,18 +78,21 @@ describe("<EcgDetail> — non-diagnostic framing", () => {
expect(html).toContain("as reported by the recording device");
});
it("says a failed read failed instead of painting an empty recording", () => {
it("shows a recoverable read failure instead of painting an empty recording", () => {
useQueryMock.mockReturnValue({
data: undefined,
isLoading: false,
isError: true,
refetch: vi.fn(),
});
const html = renderToStaticMarkup(
<I18nProvider initialLocale="en">
<EcgDetail recordingId="ecg_1" />
</I18nProvider>,
);
expect(html).toContain('data-slot="ecg-detail-error"');
// The failure is a recoverable card, not a muted "no data" line.
expect(html).toContain('data-slot="query-error-card"');
expect(html).toContain('data-slot="query-error-retry"');
expect(html).not.toContain('data-slot="ecg-trace"');
});
});
+8 -9
View File
@@ -7,6 +7,7 @@ import { apiGet } from "@/lib/api/api-fetch";
import { useTranslations, useFormatters } from "@/lib/i18n/context";
import { EcgWaveform } from "@/components/insights/ecg-waveform";
import { InsightSectionCard } from "@/components/insights/insight-section-card";
import { QueryErrorCard } from "@/components/ui/query-error-card";
import {
ecgResultLabel,
isNonNormalEcg,
@@ -41,7 +42,7 @@ export function EcgDetail({ recordingId }: { recordingId: string }) {
const { t } = useTranslations();
const fmt = useFormatters();
const { data, isLoading, isError } = useQuery({
const { data, isLoading, isError, refetch } = useQuery({
queryKey: queryKeys.insightsEcgDetail(recordingId, false),
queryFn: async () => {
try {
@@ -55,15 +56,13 @@ export function EcgDetail({ recordingId }: { recordingId: string }) {
});
if (isError) {
// A failed read must never read as "no data" (UI-STANDARDS §6).
// A failed read must never read as "no data" (UI-STANDARDS §6), and the
// alert is content, not muted meta — it also has to be recoverable.
return (
<p
data-slot="ecg-detail-error"
role="alert"
className="text-muted-foreground text-sm"
>
{t("insights.ecg.loadError")}
</p>
<QueryErrorCard
title={t("insights.ecg.loadError")}
onRetry={() => refetch()}
/>
);
}