diff --git a/src/components/insights/derived/vitals-dashboard.tsx b/src/components/insights/derived/vitals-dashboard.tsx
index a643b3c67..a1bfb06cd 100644
--- a/src/components/insights/derived/vitals-dashboard.tsx
+++ b/src/components/insights/derived/vitals-dashboard.tsx
@@ -1,9 +1,9 @@
"use client";
import { useMemo, type ReactNode } from "react";
-import { Footprints, Gauge, HeartPulse, RefreshCw, Scale } from "lucide-react";
+import { Footprints, Gauge, HeartPulse, Scale } from "lucide-react";
import { cn } from "@/lib/utils";
-import { Button } from "@/components/ui/button";
+import { QueryErrorRow } from "@/components/ui/query-error-row";
import { Skeleton } from "@/components/ui/skeleton";
import { useTranslations, useFormatters } from "@/lib/i18n/context";
import { getUnitForType } from "@/lib/validations/measurement";
@@ -744,24 +744,12 @@ export function VitalsDashboard({ batch, layout, className }: DashboardProps) {
title={t("insights.derived.vitals.sectionTitle")}
subtitle={t("insights.derived.vitals.sectionSubtitle")}
/>
-
- {t("insights.derived.vitals.loadError")}
-
-
+ refetch()}
+ />
);
diff --git a/src/components/insights/healthkit-metric-page.tsx b/src/components/insights/healthkit-metric-page.tsx
index e551109f1..093cb3129 100644
--- a/src/components/insights/healthkit-metric-page.tsx
+++ b/src/components/insights/healthkit-metric-page.tsx
@@ -3,7 +3,7 @@
import Link from "next/link";
import type { ComponentProps, ComponentType, ReactNode } from "react";
-import { RefreshCw, Sparkles } from "lucide-react";
+import { Sparkles } from "lucide-react";
import { useAuth } from "@/hooks/use-auth";
import { useUnitDisplay } from "@/hooks/use-unit-display";
@@ -15,6 +15,7 @@ import type { InsightMetric } from "@/lib/insights/metric-availability";
import type { MetricStatusMetricId } from "@/lib/insights/metric-status-registry";
import type { ChartOverlayKey } from "@/lib/dashboard-layout";
import { Button } from "@/components/ui/button";
+import { QueryErrorRow } from "@/components/ui/query-error-row";
import { Card, CardContent } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { ChartSkeleton } from "@/components/charts/chart-skeleton";
@@ -342,24 +343,12 @@ export function HealthKitMetricPage({
description={description}
explainerMetric={explainerMetric}
>
-
- {t("insights.subPage.loadError")}
-
-
+ refetch()}
+ />
);
}
diff --git a/src/components/ui/__tests__/query-error-row.test.tsx b/src/components/ui/__tests__/query-error-row.test.tsx
new file mode 100644
index 000000000..6e30b88dc
--- /dev/null
+++ b/src/components/ui/__tests__/query-error-row.test.tsx
@@ -0,0 +1,61 @@
+import { describe, expect, it } from "vitest";
+import { renderToStaticMarkup } from "react-dom/server";
+
+import { I18nProvider } from "@/lib/i18n/context";
+import { QueryErrorRow } from "../query-error-row";
+
+function ssr(node: React.ReactNode) {
+ return renderToStaticMarkup(
+ {node},
+ );
+}
+
+describe("", () => {
+ it("renders the compact alert row with foreground message text", () => {
+ const html = ssr();
+ expect(html).toContain('role="alert"');
+ expect(html).toContain('data-slot="query-error-row"');
+ expect(html).toContain("Could not load vitals");
+ // §3 — an alert IS the surface's content, so it is foreground. Both
+ // hand-rolled copies this primitive replaces put it in the muted tier.
+ expect(html).toContain("text-foreground");
+ // The compact row shape, not the tall centred ``.
+ expect(html).toContain("p-4");
+ expect(html).not.toContain("py-10");
+ });
+
+ it("falls back to the generic load-failed copy", () => {
+ expect(ssr()).toContain("Could not load");
+ });
+
+ it("only renders the retry affordance when a handler is wired", () => {
+ expect(ssr()).not.toContain("