diff --git a/messages/de.json b/messages/de.json
index fc2c4d664..04aa733f6 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -1539,11 +1539,11 @@
"bmiEmptyTitle": "Größe hinzufügen für BMI",
"bmiEmptyDescription": "Für den BMI wird deine Körpergröße benötigt. Hinterlege sie unter Einstellungen → Account.",
"bmiEmptyAction": "Account-Einstellungen öffnen",
- "noAnalysisYet": "Noch keine Analyse vorhanden.",
+ "noAnalysisYet": "Noch keine Einschätzung vorhanden.",
"assessmentPreparing": "Einschätzung wird vorbereitet…",
- "noProviderConfigured": "Auswertung nicht verfügbar.",
- "startAnalysis": "Analyse starten",
- "refreshAnalysis": "Analyse aktualisieren",
+ "noProviderConfigured": "Einschätzung nicht verfügbar.",
+ "startAnalysis": "Einschätzung starten",
+ "refreshAnalysis": "Einschätzung aktualisieren",
"keyTakeaway": "Das Wichtigste",
"findingsTitle": "Befunde",
"heroFindingPositive": "Wichtigster Befund",
@@ -1590,11 +1590,11 @@
"heroPersonalBaseline": "Basierend auf deinen letzten 90 Tagen",
"heroRegenerate": "Neu generieren",
"heroRegenerating": "Wird generiert",
- "regenerateAnalysis": "Analyse neu starten",
- "regenerateSuccess": "Analyse wurde neu erstellt",
- "warmAssessments": "Auswertungen vorbereiten",
- "warmAssessmentsHint": "Auswertungen werden über Nacht automatisch aktualisiert. Bereite sie jetzt vor, um den neuesten Stand zu sehen.",
- "warmStarted": "Auswertungen werden im Hintergrund erstellt",
+ "regenerateAnalysis": "Einschätzung neu erstellen",
+ "regenerateSuccess": "Einschätzung wurde neu erstellt",
+ "warmAssessments": "Einschätzungen vorbereiten",
+ "warmAssessmentsHint": "Einschätzungen werden über Nacht automatisch aktualisiert. Bereite sie jetzt vor, um den neuesten Stand zu sehen.",
+ "warmStarted": "Einschätzungen werden im Hintergrund erstellt",
"narrativeTitle": "Dein Zeitraum im Rückblick",
"narrativeWeek": "Diese Woche",
"narrativeMonth": "Dieser Monat",
diff --git a/messages/en.json b/messages/en.json
index 8aebe5038..3858e3844 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -1533,7 +1533,7 @@
"notAvailableForCompliance": "Comparison N/A for compliance"
},
"insights": {
- "noAnalysisYet": "No analysis yet.",
+ "noAnalysisYet": "No assessment yet.",
"assessmentPreparing": "Preparing your assessment…",
"emptyTitle": "Not enough data yet",
"emptyDescription": "Insights light up once you've logged a handful of measurements. Start with weight or blood pressure.",
@@ -1542,8 +1542,8 @@
"bmiEmptyDescription": "BMI needs your height. Add it under Settings → Account.",
"bmiEmptyAction": "Open account settings",
"noProviderConfigured": "Assessment unavailable.",
- "startAnalysis": "Start analysis",
- "refreshAnalysis": "Refresh analysis",
+ "startAnalysis": "Start assessment",
+ "refreshAnalysis": "Refresh assessment",
"keyTakeaway": "Key Takeaway",
"findingsTitle": "Findings",
"heroFindingPositive": "Top finding",
@@ -1590,8 +1590,8 @@
"heroPersonalBaseline": "Based on your last 90 days",
"heroRegenerate": "Regenerate",
"heroRegenerating": "Regenerating",
- "regenerateAnalysis": "Re-run analysis",
- "regenerateSuccess": "Analysis refreshed",
+ "regenerateAnalysis": "Re-run assessment",
+ "regenerateSuccess": "Assessment refreshed",
"warmAssessments": "Prepare assessments",
"warmAssessmentsHint": "Assessments refresh overnight automatically. Prepare them now to read the latest.",
"warmStarted": "Assessments are being prepared in the background",
diff --git a/messages/es.json b/messages/es.json
index 2e4324c1e..e525013dd 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -1533,17 +1533,17 @@
"notAvailableForCompliance": "Comparación no disponible para el cumplimiento"
},
"insights": {
- "noAnalysisYet": "Aún no hay análisis.",
- "assessmentPreparing": "Preparando tu evaluación…",
+ "noAnalysisYet": "Aún no hay valoración.",
+ "assessmentPreparing": "Preparando tu valoración…",
"emptyTitle": "Aún no hay datos suficientes",
"emptyDescription": "Los insights aparecen en cuanto registres algunas mediciones. Empieza por peso o tensión arterial.",
"emptyAddMeasurement": "Registrar medición",
"bmiEmptyTitle": "Añade tu altura para calcular el IMC",
"bmiEmptyDescription": "El IMC necesita tu altura. Guárdala en Ajustes → Cuenta.",
"bmiEmptyAction": "Abrir ajustes de cuenta",
- "noProviderConfigured": "Análisis no disponible.",
- "startAnalysis": "Iniciar análisis",
- "refreshAnalysis": "Actualizar análisis",
+ "noProviderConfigured": "Valoración no disponible.",
+ "startAnalysis": "Iniciar valoración",
+ "refreshAnalysis": "Actualizar valoración",
"keyTakeaway": "Lo más importante",
"findingsTitle": "Hallazgos",
"heroFindingPositive": "Hallazgo principal",
@@ -1590,11 +1590,11 @@
"heroPersonalBaseline": "Basado en tus últimos 90 días",
"heroRegenerate": "Regenerar",
"heroRegenerating": "Generando",
- "regenerateAnalysis": "Volver a iniciar el análisis",
- "regenerateSuccess": "Análisis regenerado",
- "warmAssessments": "Preparar evaluaciones",
- "warmAssessmentsHint": "Las evaluaciones se actualizan automáticamente durante la noche. Prepáralas ahora para ver lo más reciente.",
- "warmStarted": "Las evaluaciones se están preparando en segundo plano",
+ "regenerateAnalysis": "Volver a generar la valoración",
+ "regenerateSuccess": "Valoración regenerada",
+ "warmAssessments": "Preparar valoraciones",
+ "warmAssessmentsHint": "Las valoraciones se actualizan automáticamente durante la noche. Prepáralas ahora para ver lo más reciente.",
+ "warmStarted": "Las valoraciones se están preparando en segundo plano",
"narrativeTitle": "Tu período en resumen",
"narrativeWeek": "Esta semana",
"narrativeMonth": "Este mes",
diff --git a/messages/fr.json b/messages/fr.json
index 10360185a..84f5cf2ea 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -1533,7 +1533,7 @@
"notAvailableForCompliance": "Comparaison indisponible pour l’observance"
},
"insights": {
- "noAnalysisYet": "Pas encore d’analyse.",
+ "noAnalysisYet": "Pas encore d’évaluation.",
"assessmentPreparing": "Préparation de votre évaluation…",
"emptyTitle": "Pas encore assez de données",
"emptyDescription": "Les insights apparaissent dès que tu auras saisi quelques mesures. Commence par le poids ou la tension.",
@@ -1541,9 +1541,9 @@
"bmiEmptyTitle": "Ajoute ta taille pour l’IMC",
"bmiEmptyDescription": "L’IMC nécessite ta taille. Renseigne-la dans Paramètres → Compte.",
"bmiEmptyAction": "Ouvrir les paramètres du compte",
- "noProviderConfigured": "Analyse indisponible.",
- "startAnalysis": "Lancer l’analyse",
- "refreshAnalysis": "Actualiser l’analyse",
+ "noProviderConfigured": "Évaluation indisponible.",
+ "startAnalysis": "Lancer l’évaluation",
+ "refreshAnalysis": "Actualiser l’évaluation",
"keyTakeaway": "L’essentiel",
"findingsTitle": "Constats",
"heroFindingPositive": "Constat principal",
@@ -1590,8 +1590,8 @@
"heroPersonalBaseline": "Basé sur tes 90 derniers jours",
"heroRegenerate": "Régénérer",
"heroRegenerating": "Génération en cours",
- "regenerateAnalysis": "Relancer l’analyse",
- "regenerateSuccess": "Analyse regénérée",
+ "regenerateAnalysis": "Relancer l’évaluation",
+ "regenerateSuccess": "Évaluation regénérée",
"warmAssessments": "Préparer les évaluations",
"warmAssessmentsHint": "Les évaluations sont actualisées automatiquement pendant la nuit. Préparez-les maintenant pour voir les dernières données.",
"warmStarted": "Les évaluations sont en cours de préparation en arrière-plan",
diff --git a/messages/it.json b/messages/it.json
index 82b6af4e4..e2f2462a7 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -1533,7 +1533,7 @@
"notAvailableForCompliance": "Confronto non disponibile per l’aderenza"
},
"insights": {
- "noAnalysisYet": "Ancora nessuna analisi.",
+ "noAnalysisYet": "Ancora nessuna valutazione.",
"assessmentPreparing": "Preparazione della valutazione…",
"emptyTitle": "Dati ancora insufficienti",
"emptyDescription": "Gli insights compaiono appena registri qualche misurazione. Inizia da peso o pressione.",
@@ -1541,9 +1541,9 @@
"bmiEmptyTitle": "Aggiungi l’altezza per il BMI",
"bmiEmptyDescription": "Per il BMI serve la tua altezza. Salvala in Impostazioni → Account.",
"bmiEmptyAction": "Apri impostazioni account",
- "noProviderConfigured": "Analisi non disponibile.",
- "startAnalysis": "Avvia analisi",
- "refreshAnalysis": "Aggiorna analisi",
+ "noProviderConfigured": "Valutazione non disponibile.",
+ "startAnalysis": "Avvia valutazione",
+ "refreshAnalysis": "Aggiorna valutazione",
"keyTakeaway": "Ciò che conta",
"findingsTitle": "Riscontri",
"heroFindingPositive": "Riscontro principale",
@@ -1590,8 +1590,8 @@
"heroPersonalBaseline": "Basato sugli ultimi 90 giorni",
"heroRegenerate": "Rigenera",
"heroRegenerating": "Generazione in corso",
- "regenerateAnalysis": "Riavvia l’analisi",
- "regenerateSuccess": "Analisi rigenerata",
+ "regenerateAnalysis": "Rigenera la valutazione",
+ "regenerateSuccess": "Valutazione rigenerata",
"warmAssessments": "Prepara le valutazioni",
"warmAssessmentsHint": "Le valutazioni si aggiornano automaticamente durante la notte. Preparale ora per vedere i dati più recenti.",
"warmStarted": "Le valutazioni vengono preparate in background",
diff --git a/messages/pl.json b/messages/pl.json
index 4562d276a..63a80ff5a 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -1533,7 +1533,7 @@
"notAvailableForCompliance": "Porównanie niedostępne dla przestrzegania"
},
"insights": {
- "noAnalysisYet": "Brak analizy.",
+ "noAnalysisYet": "Brak oceny.",
"assessmentPreparing": "Przygotowywanie oceny…",
"emptyTitle": "Jeszcze za mało danych",
"emptyDescription": "Insights pojawią się, gdy zarejestrujesz kilka pomiarów. Zacznij od wagi lub ciśnienia.",
@@ -1541,9 +1541,9 @@
"bmiEmptyTitle": "Dodaj wzrost, aby obliczyć BMI",
"bmiEmptyDescription": "BMI wymaga twojego wzrostu. Zapisz go w Ustawieniach → Konto.",
"bmiEmptyAction": "Otwórz ustawienia konta",
- "noProviderConfigured": "Analiza niedostępna.",
- "startAnalysis": "Uruchom analizę",
- "refreshAnalysis": "Odśwież analizę",
+ "noProviderConfigured": "Ocena niedostępna.",
+ "startAnalysis": "Uruchom ocenę",
+ "refreshAnalysis": "Odśwież ocenę",
"keyTakeaway": "Najważniejsze",
"findingsTitle": "Wnioski",
"heroFindingPositive": "Najważniejszy wniosek",
@@ -1590,11 +1590,11 @@
"heroPersonalBaseline": "Na podstawie ostatnich 90 dni",
"heroRegenerate": "Wygeneruj ponownie",
"heroRegenerating": "Generowanie",
- "regenerateAnalysis": "Uruchom analizę ponownie",
- "regenerateSuccess": "Analiza wygenerowana ponownie",
- "warmAssessments": "Przygotuj analizy",
- "warmAssessmentsHint": "Analizy odświeżają się automatycznie w nocy. Przygotuj je teraz, aby zobaczyć najnowsze dane.",
- "warmStarted": "Analizy są przygotowywane w tle",
+ "regenerateAnalysis": "Uruchom ocenę ponownie",
+ "regenerateSuccess": "Ocena wygenerowana ponownie",
+ "warmAssessments": "Przygotuj oceny",
+ "warmAssessmentsHint": "Oceny odświeżają się automatycznie w nocy. Przygotuj je teraz, aby zobaczyć najnowsze dane.",
+ "warmStarted": "Oceny są przygotowywane w tle",
"narrativeTitle": "Twój okres w skrócie",
"narrativeWeek": "Ten tydzień",
"narrativeMonth": "Ten miesiąc",
diff --git a/src/app/insights/__tests__/assessment-spine-order.test.ts b/src/app/insights/__tests__/assessment-spine-order.test.ts
new file mode 100644
index 000000000..49e85c895
--- /dev/null
+++ b/src/app/insights/__tests__/assessment-spine-order.test.ts
@@ -0,0 +1,85 @@
+import { describe, it, expect } from "vitest";
+import { readFileSync } from "node:fs";
+import { join } from "node:path";
+
+/**
+ * v1.12.2 — canonical metric-detail spine guard.
+ *
+ * v1.12.0 made the AI assessment the LAST content block on every metric
+ * sub-page (primary → stat strip → chart → range → target → forecast →
+ * assessment). The generic `HealthKitMetricPage` scaffold enforces this for
+ * the ~29 HealthKit pages structurally; the six bespoke pages each hand-write
+ * their body, so a future edit can silently slot a card after the assessment
+ * (as `mood` and `medications` did before this release).
+ *
+ * This is a source-level structural guard: the assessment card element must be
+ * the last JSX child of the page's ``. Asserting on the source
+ * (rather than rendered markup) keeps the test deterministic — the preceding
+ * blocks (`MetricTargetSummary`, `MoodInsightsSections`, `TherapyTimeline`)
+ * all self-suppress on a data miss, so a render-order assertion would depend
+ * on mocking the whole data layer.
+ */
+
+const PAGES_DIR = join(process.cwd(), "src", "app", "insights");
+
+// Each bespoke page + the element that mounts its assessment card. The five
+// `useInsightStatus`-backed pages render ``; the
+// medications page reads the richer `summary`-shaped route inline, so it
+// renders `` directly.
+const BESPOKE_PAGES: ReadonlyArray<{ slug: string; assessmentTag: string }> = [
+ { slug: "weight", assessmentTag: " {
+ for (const { slug, assessmentTag } of BESPOKE_PAGES) {
+ it(`renders the assessment card as the last SubPageShell child on /insights/${slug}`, () => {
+ const source = readFileSync(
+ join(PAGES_DIR, slug, "page.tsx"),
+ "utf8",
+ );
+
+ // The render branch that carries the assessment is the final
+ // `…` in the module (the empty-state
+ // branches close their own shell earlier and never mount the card).
+ const shellClose = source.lastIndexOf("");
+ expect(shellClose, `${slug}: no closing `).toBeGreaterThan(
+ -1,
+ );
+ const shellOpen = source.lastIndexOf("` that closes it, then assert nothing but
+ // whitespace separates that close from ``. The icon
+ // passed as a prop (``) self-closes BEFORE the element's own
+ // `/>`, so the first `/>` after the assessment open is the icon's and
+ // the second is the element's — scan for the last `/>` in the body,
+ // which belongs to the trailing assessment element by construction.
+ const selfClose = body.lastIndexOf("/>");
+ expect(
+ selfClose,
+ `${slug}: assessment card is not self-closing as expected`,
+ ).toBeGreaterThan(assessmentIndex);
+
+ const trailing = body.slice(selfClose + 2).trim();
+ expect(
+ trailing,
+ `${slug}: "${trailing.slice(0, 40)}" renders AFTER the assessment card — ` +
+ `the assessment must be the last block on the canonical spine`,
+ ).toBe("");
+ });
+ }
+});
diff --git a/src/app/insights/blood-pressure/page.tsx b/src/app/insights/blood-pressure/page.tsx
index 12553fc69..a6bbc6922 100644
--- a/src/app/insights/blood-pressure/page.tsx
+++ b/src/app/insights/blood-pressure/page.tsx
@@ -4,13 +4,12 @@ import Link from "next/link";
import { HeartPulse } from "lucide-react";
import { useAuth } from "@/hooks/use-auth";
-import { useInsightStatus } from "@/hooks/use-insight-status";
import { useInsightsAnalytics } from "@/hooks/use-insights-analytics";
import { useTranslations } from "@/lib/i18n/context";
import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs";
import { Button } from "@/components/ui/button";
import { HealthChartDynamic } from "@/components/charts/health-chart-dynamic";
-import { InsightStatusCard } from "@/components/insights/insight-status-card";
+import { SlugInsightStatusCard } from "@/components/insights/slug-insight-status-card";
import { MeasurementDiversityNudge } from "@/components/insights/measurement-diversity-nudge";
import { MetricEmptyState } from "@/components/insights/metric-empty-state";
import { MetricLastMeasurementCard } from "@/components/insights/metric-last-measurement-card";
@@ -44,9 +43,6 @@ export default function InsightsBlutdruckPage() {
const { t } = useTranslations();
const { compareBaseline } = useInsightsLayoutPrefs(user != null);
- const { data: status, isLoading: isStatusLoading } =
- useInsightStatus("blood-pressure");
-
const { data: analytics, isEmpty } =
useInsightsAnalytics("BLOOD_PRESSURE_SYS");
const bpLastSeenAt =
@@ -147,14 +143,9 @@ export default function InsightsBlutdruckPage() {
{/* v1.12.0 — Einschätzung is the last block on the canonical
metric-detail spine. */}
- }
- text={status?.text ?? null}
- hasProvider={status?.hasProvider ?? false}
- updatedAt={status?.updatedAt ?? null}
- loading={isStatusLoading}
- preparing={status?.preparing ?? false}
/>
);
diff --git a/src/app/insights/bmi/page.tsx b/src/app/insights/bmi/page.tsx
index 5b74d78dc..7d3788a3d 100644
--- a/src/app/insights/bmi/page.tsx
+++ b/src/app/insights/bmi/page.tsx
@@ -4,7 +4,6 @@ import Link from "next/link";
import { Ruler } from "lucide-react";
import { useAuth } from "@/hooks/use-auth";
-import { useInsightStatus } from "@/hooks/use-insight-status";
import { useInsightsAnalytics } from "@/hooks/use-insights-analytics";
import { useTranslations } from "@/lib/i18n/context";
import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs";
@@ -12,7 +11,7 @@ import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/ui/empty-state";
import { HealthChartDynamic } from "@/components/charts/health-chart-dynamic";
import { CoachLaunchButton } from "@/components/insights/coach-launch-button";
-import { InsightStatusCard } from "@/components/insights/insight-status-card";
+import { SlugInsightStatusCard } from "@/components/insights/slug-insight-status-card";
import { MetricEmptyState } from "@/components/insights/metric-empty-state";
import { MetricRangeControls } from "@/components/insights/metric-range-controls";
import { MetricTargetSummary } from "@/components/insights/metric-target-summary";
@@ -45,8 +44,6 @@ export default function InsightsBmiPage() {
const { t } = useTranslations();
const { compareBaseline } = useInsightsLayoutPrefs(isAuthenticated);
- const { data: status, isLoading: isStatusLoading } = useInsightStatus("bmi");
-
const { isEmpty } = useInsightsAnalytics("BMI");
// v1.4.27 F17 — BMI is derived from WEIGHT. When no weight readings
@@ -137,15 +134,7 @@ export default function InsightsBmiPage() {
- }
- text={status?.text ?? null}
- hasProvider={status?.hasProvider ?? false}
- updatedAt={status?.updatedAt ?? null}
- loading={isStatusLoading}
- preparing={status?.preparing ?? false}
- />
+ } />
);
}
diff --git a/src/app/insights/medications/page.tsx b/src/app/insights/medications/page.tsx
index 835f561e2..5c5e3227e 100644
--- a/src/app/insights/medications/page.tsx
+++ b/src/app/insights/medications/page.tsx
@@ -283,6 +283,18 @@ export default function InsightsMedikamentePage() {
+ {/* v1.4.25 W4d — GLP-1 therapy timeline. Self-hides for users
+ without an active GLP-1 medication, so the page collapses
+ back to the compliance grid for everyone else. */}
+
+
+ {/* v1.12.2 — the assessment is the LAST block on every bespoke
+ metric-detail page, matching the canonical spine the generic
+ scaffold renders. This card reads the medication-compliance
+ status route, which carries a richer envelope (`summary` +
+ per-medication `text`) than the standard `text`-only generators,
+ so it keeps its inline wiring rather than the shared
+ `` seam. */}
}
@@ -292,11 +304,6 @@ export default function InsightsMedikamentePage() {
loading={isStatusLoading}
preparing={status?.preparing ?? false}
/>
-
- {/* v1.4.25 W4d — GLP-1 therapy timeline. Self-hides for users
- without an active GLP-1 medication, so the page collapses
- back to the compliance grid for everyone else. */}
-
);
}
diff --git a/src/app/insights/mood/page.tsx b/src/app/insights/mood/page.tsx
index 9f3d3d9fc..a49461cbd 100644
--- a/src/app/insights/mood/page.tsx
+++ b/src/app/insights/mood/page.tsx
@@ -6,16 +6,15 @@ import Link from "next/link";
import { Smile } from "lucide-react";
import { useAuth } from "@/hooks/use-auth";
-import { useInsightStatus } from "@/hooks/use-insight-status";
import { queryKeys } from "@/lib/query-keys";
import { useTranslations } from "@/lib/i18n/context";
import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs";
import { Button } from "@/components/ui/button";
import { ChartSkeleton } from "@/components/charts/chart-skeleton";
-import { InsightStatusCard } from "@/components/insights/insight-status-card";
import { MetricEmptyState } from "@/components/insights/metric-empty-state";
import { MetricTargetSummary } from "@/components/insights/metric-target-summary";
import { MoodInsightsSections } from "@/components/insights/mood/mood-insights-sections";
+import { SlugInsightStatusCard } from "@/components/insights/slug-insight-status-card";
import { SubPageShell } from "@/components/insights/sub-page-shell";
/**
@@ -45,8 +44,6 @@ export default function InsightsStimmungPage() {
const { t } = useTranslations();
const { compareBaseline } = useInsightsLayoutPrefs(isAuthenticated);
- const { data: status, isLoading: isStatusLoading } = useInsightStatus("mood");
-
// Reuse the mother-page comprehensive query — TanStack Query
// dedups so this is a free cache read for the common case.
const { data: comprehensive } = useQuery({
@@ -110,22 +107,16 @@ export default function InsightsStimmungPage() {
userTimezone={user?.timezone}
/>
- {/* v1.8.7 — the AI assessment reads best directly under the first
- chart: the reader sees the trend, then the narration of it,
- before the calendar / distribution / correlation breakdowns. */}
- }
- text={status?.text ?? null}
- hasProvider={status?.hasProvider ?? false}
- updatedAt={status?.updatedAt ?? null}
- loading={isStatusLoading}
- preparing={status?.preparing ?? false}
- />
-
+
+ {/* v1.12.2 — the assessment is the LAST block on every bespoke
+ metric-detail page, matching the canonical spine the generic
+ scaffold (weight / bmi / pulse / blood-pressure) renders. The
+ reader sees the trend and the breakdown sections first, then the
+ narration of them at the foot. */}
+ } />
);
}
diff --git a/src/app/insights/pulse/page.tsx b/src/app/insights/pulse/page.tsx
index b2186faba..50834520c 100644
--- a/src/app/insights/pulse/page.tsx
+++ b/src/app/insights/pulse/page.tsx
@@ -4,13 +4,12 @@ import Link from "next/link";
import { Heart } from "lucide-react";
import { useAuth } from "@/hooks/use-auth";
-import { useInsightStatus } from "@/hooks/use-insight-status";
import { useInsightsAnalytics } from "@/hooks/use-insights-analytics";
import { useTranslations } from "@/lib/i18n/context";
import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs";
import { Button } from "@/components/ui/button";
import { HealthChartDynamic } from "@/components/charts/health-chart-dynamic";
-import { InsightStatusCard } from "@/components/insights/insight-status-card";
+import { SlugInsightStatusCard } from "@/components/insights/slug-insight-status-card";
import { MetricEmptyState } from "@/components/insights/metric-empty-state";
import { MetricStatStrip } from "@/components/insights/metric-stat-strip";
import { MetricPrimaryTile } from "@/components/insights/metric-primary-tile";
@@ -46,9 +45,6 @@ export default function InsightsPulsPage() {
const { t } = useTranslations();
const { compareBaseline } = useInsightsLayoutPrefs(isAuthenticated);
- const { data: status, isLoading: isStatusLoading } =
- useInsightStatus("pulse");
-
// v1.4.25 W16a — VO2 max chart-row consumes the same `/api/analytics`
// bundle the mother page reads. Sharing the cache key keeps the
// payload single-fetch on tab navigation (React-Query unwraps from
@@ -171,15 +167,7 @@ export default function InsightsPulsPage() {
{/* v1.12.0 — Einschätzung is the last block on the canonical
metric-detail spine. */}
- }
- text={status?.text ?? null}
- hasProvider={status?.hasProvider ?? false}
- updatedAt={status?.updatedAt ?? null}
- loading={isStatusLoading}
- preparing={status?.preparing ?? false}
- />
+ } />
);
}
diff --git a/src/app/insights/weight/page.tsx b/src/app/insights/weight/page.tsx
index bce1b93a8..391c8de98 100644
--- a/src/app/insights/weight/page.tsx
+++ b/src/app/insights/weight/page.tsx
@@ -4,13 +4,12 @@ import Link from "next/link";
import { Scale } from "lucide-react";
import { useAuth } from "@/hooks/use-auth";
-import { useInsightStatus } from "@/hooks/use-insight-status";
import { useInsightsAnalytics } from "@/hooks/use-insights-analytics";
import { useTranslations } from "@/lib/i18n/context";
import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs";
import { Button } from "@/components/ui/button";
import { HealthChartDynamic } from "@/components/charts/health-chart-dynamic";
-import { InsightStatusCard } from "@/components/insights/insight-status-card";
+import { SlugInsightStatusCard } from "@/components/insights/slug-insight-status-card";
import { MetricEmptyState } from "@/components/insights/metric-empty-state";
import { MetricStatStrip } from "@/components/insights/metric-stat-strip";
import { MetricPrimaryTile } from "@/components/insights/metric-primary-tile";
@@ -40,9 +39,6 @@ export default function InsightsGewichtPage() {
const { t } = useTranslations();
const { compareBaseline } = useInsightsLayoutPrefs(isAuthenticated);
- const { data: status, isLoading: isStatusLoading } =
- useInsightStatus("weight");
-
const { data: analytics, isEmpty } = useInsightsAnalytics("WEIGHT");
const weightSummary = analytics?.summaries?.WEIGHT ?? null;
const weightLastSeenAt =
@@ -128,15 +124,7 @@ export default function InsightsGewichtPage() {
off the overview onto its metric page). */}
- }
- text={status?.text ?? null}
- hasProvider={status?.hasProvider ?? false}
- updatedAt={status?.updatedAt ?? null}
- loading={isStatusLoading}
- preparing={status?.preparing ?? false}
- />
+ } />
);
}
diff --git a/src/components/insights/__tests__/insight-status-card.test.tsx b/src/components/insights/__tests__/insight-status-card.test.tsx
index b58fcda94..6ecabe3fc 100644
--- a/src/components/insights/__tests__/insight-status-card.test.tsx
+++ b/src/components/insights/__tests__/insight-status-card.test.tsx
@@ -86,7 +86,8 @@ describe("", () => {
it("renders the empty-text state without crashing on null", () => {
const html = render();
- expect(html).toContain("No analysis yet.");
+ // v1.12.2 — the empty state shares the canonical assessment noun.
+ expect(html).toContain("No assessment yet.");
});
it("never surfaces a cached badge — the card has no cached affordance", () => {
diff --git a/src/components/insights/insight-status-card.tsx b/src/components/insights/insight-status-card.tsx
index 0f1680ca2..b32396919 100644
--- a/src/components/insights/insight-status-card.tsx
+++ b/src/components/insights/insight-status-card.tsx
@@ -3,7 +3,8 @@
import { useEffect, useRef, useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
-import { useTranslations, useFormatters } from "@/lib/i18n/context";
+import { useTranslations } from "@/lib/i18n/context";
+import { formatRelativeTime } from "@/lib/i18n/relative-time";
import { stripChartTokens } from "@/lib/insights/chart-tokens";
import { cn } from "@/lib/utils";
import { useFeatureFlags } from "@/hooks/use-feature-flags";
@@ -154,6 +155,9 @@ export function InsightStatusCard({
// on the success card closes the load→loaded transition signal.
- {t("insights.lastUpdated")}: {fmt.dateTime(updatedAt)}
+ {t("insights.lastUpdated")}: {formatRelativeTime(updatedAt, t)}
);
}
diff --git a/src/components/insights/slug-insight-status-card.tsx b/src/components/insights/slug-insight-status-card.tsx
new file mode 100644
index 000000000..39c00311e
--- /dev/null
+++ b/src/components/insights/slug-insight-status-card.tsx
@@ -0,0 +1,48 @@
+"use client";
+
+import type { ReactNode } from "react";
+
+import {
+ useInsightStatus,
+ type InsightStatusMetric,
+} from "@/hooks/use-insight-status";
+import { useTranslations } from "@/lib/i18n/context";
+import { InsightStatusCard } from "@/components/insights/insight-status-card";
+
+interface SlugInsightStatusCardProps {
+ /** The bespoke metric slug the `/api/insights/-status` route keys on. */
+ slug: InsightStatusMetric;
+ /** The leading glyph (Scale, Smile, Pill, …). */
+ icon: ReactNode;
+}
+
+/**
+ * Shared mount for the bespoke per-metric assessment card.
+ *
+ * The five bespoke slug pages (`weight`, `bmi`, `pulse`, `blood-pressure`,
+ * `mood`) each hand-wired the same eight-prop `` block off
+ * `useInsightStatus(slug)`, with only the icon differing. This seam owns the
+ * hook + the `status?.x ?? default` prop-defaulting so the title and every
+ * field name live in one place — the sibling of ``, which
+ * does the same for the generic `useInsightMetricStatus(metric)` route. Each
+ * bespoke page drops ~9 lines and can no longer drift on a field name.
+ */
+export function SlugInsightStatusCard({
+ slug,
+ icon,
+}: SlugInsightStatusCardProps) {
+ const { t } = useTranslations();
+ const { data: status, isLoading } = useInsightStatus(slug);
+
+ return (
+
+ );
+}
diff --git a/src/hooks/use-insight-status.ts b/src/hooks/use-insight-status.ts
index db3b92279..43f93d702 100644
--- a/src/hooks/use-insight-status.ts
+++ b/src/hooks/use-insight-status.ts
@@ -102,7 +102,7 @@ export function nextStatusPollInterval(
* `["insights", "weight-status", locale]` array typoed once is exactly
* the class of bug `queryKeys` was introduced to defend against.
*/
-type InsightStatusMetric =
+export type InsightStatusMetric =
| "blood-pressure"
| "weight"
| "pulse"