diff --git a/src/components/insights/mood/__tests__/mood-insights-sections-error-state.test.tsx b/src/components/insights/mood/__tests__/mood-insights-sections-error-state.test.tsx
new file mode 100644
index 000000000..7df15c84d
--- /dev/null
+++ b/src/components/insights/mood/__tests__/mood-insights-sections-error-state.test.tsx
@@ -0,0 +1,53 @@
+/**
+ * The mood-insights regions used to render nothing on a failed read, by
+ * documented intent ("degrades gracefully to the line chart"). The maintainer
+ * decision is to enforce §6 here like everywhere else: a failed read is not an
+ * empty page. The shared query is forced into its error state; the main "rest"
+ * region has to surface a `query-error-row` with retry (mirroring the loading
+ * convention, which also only paints in the "rest" region).
+ */
+import { describe, it, expect, vi } from "vitest";
+import { renderToStaticMarkup } from "react-dom/server";
+
+vi.mock("@/hooks/use-auth", () => ({
+ useAuth: () => ({
+ user: { id: "u1" },
+ isLoading: false,
+ isAuthenticated: true,
+ error: null,
+ refetch: () => {},
+ }),
+}));
+
+vi.mock("@tanstack/react-query", () => ({
+ useQuery: () => ({
+ data: undefined,
+ isLoading: false,
+ isError: true,
+ refetch: vi.fn(),
+ }),
+}));
+
+import { I18nProvider } from "@/lib/i18n/context";
+import { MoodInsightsSections } from "../mood-insights-sections";
+
+function render(region: "heatmap" | "assessment" | "rest") {
+ return renderToStaticMarkup(
+
+
+ ,
+ );
+}
+
+describe(" — a read failure is honest, not empty", () => {
+ it("surfaces a query-error row with retry in the rest region", () => {
+ const html = render("rest");
+ expect(html).toContain('data-slot="query-error-row"');
+ expect(html).toContain('data-slot="query-error-row-retry"');
+ });
+
+ it("stays quiet in the heatmap/assessment regions to avoid stacking rows", () => {
+ expect(render("heatmap")).toBe("");
+ expect(render("assessment")).toBe("");
+ });
+});
diff --git a/src/components/insights/mood/mood-insights-sections.tsx b/src/components/insights/mood/mood-insights-sections.tsx
index be65c1124..a5de9a854 100644
--- a/src/components/insights/mood/mood-insights-sections.tsx
+++ b/src/components/insights/mood/mood-insights-sections.tsx
@@ -7,6 +7,7 @@ import { CalendarDays, Sparkles } from "lucide-react";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { TileHeader } from "@/components/insights/tile-header";
+import { QueryErrorRow } from "@/components/ui/query-error-row";
import { Skeleton } from "@/components/ui/skeleton";
import { useAuth } from "@/hooks/use-auth";
import { queryKeys } from "@/lib/query-keys";
@@ -100,7 +101,7 @@ export function MoodInsightsSections({
const { isAuthenticated } = useAuth();
const { t } = useTranslations();
- const { data, isLoading } = useQuery({
+ const { data, isLoading, isError, refetch } = useQuery({
queryKey: queryKeys.moodInsights(),
queryFn: async () => {
return apiGet("/api/mood/insights");
@@ -118,6 +119,15 @@ export function MoodInsightsSections({
) : null;
}
+ if (isError) {
+ // §6 — a failed read is not an empty page. Surface it once, in the main
+ // "rest" region (mirroring the loading convention above), rather than
+ // stacking an identical row in all three regions down the page.
+ return region === "rest" ? (
+ refetch()} />
+ ) : null;
+ }
+
if (!data || data.summary.totalEntries === 0) {
return null;
}