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"