fix(insights): dedupe coach disclaimer and add wellness-strip error affordance

Suppress the generic Insights disclaimer footer on the Coach route, which
already renders its own composer disclaimer — the layout shell now skips
the footer when the selected segment is the coach page and keeps it
everywhere else. Thread the shared derived-batch error state and refetch
into the wellness strip so a failed batch surfaces a compact error +
Retry in place (the strip is top-of-page) instead of vanishing silently;
the one Retry recovers both the wellness and vitals strips. Adds a
scores load-error string across all locales.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.8 committed 2026-06-05 12:58:39 +02:00
1 parent aac70ec765
commit 22ba4193b2
9 files changed
+82 -9

No files matched your search

+1
View File
@@ -2772,6 +2772,7 @@
},
"scores": {
"sectionTitle": "Wellness-Werte",
"loadError": "Deine Wellness-Werte konnten gerade nicht geladen werden.",
"recovery": "Erholung",
"stress": "Stress",
"strain": "Belastung"
+1
View File
@@ -2772,6 +2772,7 @@
},
"scores": {
"sectionTitle": "Wellness scores",
"loadError": "Could not load your wellness scores right now.",
"recovery": "Recovery",
"stress": "Stress",
"strain": "Strain"
+1
View File
@@ -2772,6 +2772,7 @@
},
"scores": {
"sectionTitle": "Puntuaciones de bienestar",
"loadError": "No se pudieron cargar tus puntuaciones de bienestar en este momento.",
"recovery": "Recuperación",
"stress": "Estrés",
"strain": "Esfuerzo"
+1
View File
@@ -2772,6 +2772,7 @@
},
"scores": {
"sectionTitle": "Scores de bien-être",
"loadError": "Impossible de charger vos scores de bien-être pour le moment.",
"recovery": "Récupération",
"stress": "Stress",
"strain": "Effort"
+1
View File
@@ -2772,6 +2772,7 @@
},
"scores": {
"sectionTitle": "Punteggi di benessere",
"loadError": "Al momento non è stato possibile caricare i tuoi punteggi di benessere.",
"recovery": "Recupero",
"stress": "Stress",
"strain": "Sforzo"
+1
View File
@@ -2772,6 +2772,7 @@
},
"scores": {
"sectionTitle": "Wyniki dobrostanu",
"loadError": "Nie udało się teraz wczytać Twoich wyników dobrostanu.",
"recovery": "Regeneracja",
"stress": "Stres",
"strain": "Obciążenie"
+2
View File
@@ -326,6 +326,8 @@ export default function InsightsPage() {
<WellnessScores
read={dashboardDerived.read}
isLoading={dashboardDerived.isLoading}
isError={dashboardDerived.isError}
refetch={dashboardDerived.refetch}
/>
{flags.briefing && (
@@ -2,8 +2,9 @@
import type { ComponentType } from "react";
import Link from "next/link";
import { Activity, Flame, Gauge, HeartPulse, Moon } from "lucide-react";
import { Activity, Flame, Gauge, HeartPulse, Moon, RefreshCw } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { useTranslations } from "@/lib/i18n/context";
import { TileHeader } from "@/components/insights/tile-header";
import { ScoreRing } from "./score-ring";
@@ -36,6 +37,15 @@ interface ScoreStripProps {
/** The batched-read selector from the parent dashboard's one query. */
read: DerivedBatchRead;
isLoading: boolean;
/**
* The shared derived-batch error flag. On a failed batch the strip renders
* a compact error + Retry in place instead of vanishing silently (the strip
* is now top-of-page, so its only Retry must live here, not far below in the
* Vitals slot).
*/
isError?: boolean;
/** Refetch the one shared batch query. Recovers both strips at once. */
refetch?: () => void;
className?: string;
}
@@ -86,9 +96,51 @@ function RingTile({
);
}
export function WellnessScores({ read, isLoading, className }: ScoreStripProps) {
export function WellnessScores({
read,
isLoading,
isError = false,
refetch,
className,
}: ScoreStripProps) {
const { t } = useTranslations();
// A failed shared batch must read as an error, not as "no scores" — the
// strip is top-of-page, so the only Retry lives here (mirrors the Vitals
// error card styling; one Retry recovers both strips).
if (isError) {
return (
<section
data-slot="wellness-scores"
aria-label={t("insights.derived.scores.sectionTitle")}
className={cn("space-y-3", className)}
>
<TileHeader
icon={Activity}
title={t("insights.derived.scores.sectionTitle")}
/>
<div
data-slot="wellness-scores-error"
role="alert"
className="bg-card border-border text-muted-foreground flex flex-col items-start gap-3 rounded-xl border p-4 text-sm sm:flex-row sm:items-center sm:justify-between"
>
<span>{t("insights.derived.scores.loadError")}</span>
<Button
type="button"
size="sm"
variant="outline"
onClick={() => refetch?.()}
data-slot="wellness-scores-retry"
className="gap-1.5"
>
<RefreshCw className="h-3.5 w-3.5" aria-hidden="true" />
<span>{t("common.retry")}</span>
</Button>
</div>
</section>
);
}
const readiness = read<ReadinessValue>({ metric: "READINESS" });
const sleep = read<SleepScoreValue>({ metric: "SLEEP_SCORE" });
const recovery = read<WellnessScoreValue>({ metric: "RECOVERY_SCORE" });
@@ -1,6 +1,7 @@
"use client";
import { useMemo, type ReactNode } from "react";
import { useSelectedLayoutSegment } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import { useTranslations } from "@/lib/i18n/context";
@@ -57,6 +58,15 @@ export function InsightsLayoutShell({ children }: { children: ReactNode }) {
const flags = useFeatureFlags();
const advisorEnabled =
isAuthenticated && flags.enabled && flags.briefing;
// v1.12.6 — routes that carry their own purpose-built disclaimer must NOT
// also show the generic footer below (otherwise two disclaimers stack).
// The Coach full page renders its own `composerDisclaimer` under the
// composer, so its segment is excluded here. Every other segment — the
// overview (null segment), the metric sub-pages, mood, medications, the
// raw values table — keeps the single generic footer.
const segment = useSelectedLayoutSegment();
const showDisclaimerFooter = segment !== "coach";
const advisor = useInsightsAdvisorQuery(advisorEnabled);
// Shared analytics fetch — the layout shell only reads
@@ -118,13 +128,16 @@ export function InsightsLayoutShell({ children }: { children: ReactNode }) {
caveats (health score, correlations, mood relations, derived cards,
rhythm events) stay on their own cards — those convey real,
non-generic information. Other waves remove the generic line from the
overview / mood / medications pages; this footer is where it lives. */}
<p
data-slot="insights-disclaimer-footer"
className="text-muted-foreground border-border/60 border-t pt-4 text-center text-xs leading-relaxed"
>
{t("insights.disclaimer.footer")}
</p>
overview / mood / medications pages; this footer is where it lives.
Suppressed on the Coach route, which carries its own disclaimer. */}
{showDisclaimerFooter ? (
<p
data-slot="insights-disclaimer-footer"
className="text-muted-foreground border-border/60 border-t pt-4 text-center text-xs leading-relaxed"
>
{t("insights.disclaimer.footer")}
</p>
) : null}
</div>
);
}