diff --git a/CHANGELOG.md b/CHANGELOG.md
index a090d8be4..9592bf143 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,18 @@
# Changelog
+## [1.8.7] — 2026-06-02 — Instant insight assessments, GLP-1 blood-level chart, tile + caption polish
+
+### Fixed
+
+- **Insight assessments are instant again.** A category's assessment was being re-prepared on almost every visit: the constant Apple Health / Withings sync kept evicting the cached text, and a miss only ever deleted-and-waited. Assessments now serve the last good text immediately (stale-while-revalidate) and refresh in the background — "preparing" shows only when an assessment has genuinely never been generated. The background refresh is also debounced per metric, so a sync burst no longer triggers a storm of regenerations, and it warms only the active language (halving the work).
+- **The dashboard tiles never truncate the value.** In a dense tile strip the number could clip or wrap mid-value (e.g. "130 mmHg" showing as "13"); the value now always renders in full and the unit yields the space instead.
+- **GLP-1 medications show the drug-level-in-blood estimate by default.** The estimated concentration curve was hidden behind the Research-Mode opt-in and below the dose-strength curve; for GLP-1 medications it now shows by default, above the dose-strength curve, with the "educational estimate, not a measurement" disclaimer kept clearly attached.
+- **The Trends captions read consistently.** Metrics without an advisor note previously rendered their caption in a different, plainer style that broke the row; every Trends caption now uses the same treatment.
+
+### Changed
+
+- **The mood assessment sits directly under the first chart** on the mood insights page.
+
## [1.8.6] — 2026-06-01 — Compliance two-row return, insights polish, targets deprecation
### Added
diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml
index 15b29b6ee..cd0f537f7 100644
--- a/docs/api/openapi.yaml
+++ b/docs/api/openapi.yaml
@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: HealthLog API
- version: 1.8.6
+ version: 1.8.7
description: >-
Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest.
diff --git a/messages/de.json b/messages/de.json
index b7043af03..dd3303220 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -1127,11 +1127,6 @@
"emptyState": "Noch keine Einnahmen erfasst.",
"emptyStateCta": "Trage eine Dosis ein, damit die geschätzte Verlaufskurve Gestalt annimmt.",
"estimateNote": "Edukative Schätzung aus EMA-publizierter Populations-Pharmakokinetik. Keine Messung.",
- "gatedTitle": "Forschungsmodus ist aus",
- "gatedBody": "Aktiviere den Forschungsmodus in den Einstellungen, um die geschätzte Wirkstoffkurve zu sehen. Die Kurve ist opt-in, weil sie eine Lernhilfe und keine klinische Messung ist.",
- "gatedStaleTitle": "Disclaimer muss neu bestätigt werden",
- "gatedStaleBody": "Der Disclaimer des Forschungsmodus wurde seit deiner letzten Bestätigung aktualisiert. Lies ihn in den Einstellungen erneut und bestätige, um die Kurve wieder einzublenden.",
- "gatedCta": "Forschungsmodus in den Einstellungen öffnen",
"unknownDrug": "{name} ist nicht im EMA-zugelassenen GLP-1-Katalog enthalten. HealthLog kann dafür keine Populations-Pharmakokinetik-Kurve erzeugen."
}
},
@@ -1306,8 +1301,7 @@
"history": {
"back": "Zurück zum Medikament",
"subtitle": "Vollständiger Einnahmeverlauf",
- "plannedSuffix": "geplant",
- "drugLevelDisclosure": "Geschätzte Wirkstoffkurve anzeigen"
+ "plannedSuffix": "geplant"
}
},
"logInjectionSiteTitle": "Injektionsstelle erfassen",
diff --git a/messages/en.json b/messages/en.json
index ab4fb72b3..94ec0d550 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -1127,11 +1127,6 @@
"emptyState": "No intake events yet.",
"emptyStateCta": "Log a dose to see the estimated level take shape.",
"estimateNote": "Educational estimate from EMA-published population pharmacokinetics. Not a measurement.",
- "gatedTitle": "Research Mode is off",
- "gatedBody": "Turn on Research Mode in Settings to see the estimated drug-level curve. The curve is opt-in because it is a learning aid, not a clinical measurement.",
- "gatedStaleTitle": "Disclaimer needs re-acknowledgment",
- "gatedStaleBody": "The Research Mode disclaimer was updated since you last acknowledged it. Re-read and confirm in Settings to bring the chart back.",
- "gatedCta": "Open Research Mode in Settings",
"unknownDrug": "{name} is not in the EMA-approved GLP-1 catalog, so HealthLog cannot render a population-PK curve for it."
}
},
@@ -1306,8 +1301,7 @@
"history": {
"back": "Back to medication",
"subtitle": "Full intake history",
- "plannedSuffix": "planned",
- "drugLevelDisclosure": "Show estimated drug-level curve"
+ "plannedSuffix": "planned"
}
},
"logInjectionSiteTitle": "Log injection site",
diff --git a/messages/es.json b/messages/es.json
index 7ae81e64d..4953bba1d 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -1127,11 +1127,6 @@
"emptyState": "Sin tomas registradas todavía.",
"emptyStateCta": "Registra una dosis para ver la curva estimada cobrar forma.",
"estimateNote": "Estimación educativa a partir de la farmacocinética poblacional publicada por la EMA. No es una medición.",
- "gatedTitle": "Modo Investigación desactivado",
- "gatedBody": "Activa el modo Investigación en los ajustes para ver la curva estimada del nivel de fármaco. La curva es de inclusión voluntaria porque es una ayuda de aprendizaje, no una medición clínica.",
- "gatedStaleTitle": "El disclaimer requiere reconfirmación",
- "gatedStaleBody": "El disclaimer del modo Investigación se actualizó desde tu última confirmación. Relelo en los ajustes y confirma de nuevo para volver a ver la curva.",
- "gatedCta": "Abrir el modo Investigación en los ajustes",
"unknownDrug": "{name} no está en el catálogo de GLP-1 aprobado por la EMA, por lo que HealthLog no puede trazar una curva de farmacocinética poblacional."
}
},
@@ -1306,8 +1301,7 @@
"history": {
"back": "Volver al medicamento",
"subtitle": "Historial completo de tomas",
- "plannedSuffix": "planificado",
- "drugLevelDisclosure": "Mostrar curva estimada del principio activo"
+ "plannedSuffix": "planificado"
}
},
"logInjectionSiteTitle": "Registrar sitio de inyección",
diff --git a/messages/fr.json b/messages/fr.json
index 30635a52f..d79dcf777 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -1127,11 +1127,6 @@
"emptyState": "Aucune prise enregistrée.",
"emptyStateCta": "Enregistrez une dose pour voir la courbe estimée prendre forme.",
"estimateNote": "Estimation éducative à partir de la pharmacocinétique de population publiée par l'EMA. Pas une mesure.",
- "gatedTitle": "Mode Recherche désactivé",
- "gatedBody": "Activez le mode Recherche dans les paramètres pour voir la courbe estimée du niveau de médicament. La courbe est en opt-in parce que c'est un outil d'apprentissage, pas une mesure clinique.",
- "gatedStaleTitle": "Le disclaimer doit être reconfirmé",
- "gatedStaleBody": "Le disclaimer du mode Recherche a été mis à jour depuis votre dernière confirmation. Relisez-le dans les paramètres et confirmez à nouveau pour réafficher la courbe.",
- "gatedCta": "Ouvrir le mode Recherche dans les paramètres",
"unknownDrug": "{name} n'est pas dans le catalogue GLP-1 approuvé par l'EMA, donc HealthLog ne peut pas tracer de courbe de pharmacocinétique de population."
}
},
@@ -1306,8 +1301,7 @@
"history": {
"back": "Retour au médicament",
"subtitle": "Historique complet des prises",
- "plannedSuffix": "prévu",
- "drugLevelDisclosure": "Afficher la courbe estimée du principe actif"
+ "plannedSuffix": "prévu"
}
},
"logInjectionSiteTitle": "Enregistrer le site d'injection",
diff --git a/messages/it.json b/messages/it.json
index dba26913b..528df33c0 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -1127,11 +1127,6 @@
"emptyState": "Nessuna assunzione registrata.",
"emptyStateCta": "Registra una dose per vedere prendere forma la curva stimata.",
"estimateNote": "Stima educativa dalla farmacocinetica di popolazione pubblicata dall'EMA. Non è una misura.",
- "gatedTitle": "Modalità Ricerca disattivata",
- "gatedBody": "Attiva la modalità Ricerca nelle impostazioni per vedere la curva stimata del livello del farmaco. La curva è opt-in perché è uno strumento di apprendimento, non una misura clinica.",
- "gatedStaleTitle": "Il disclaimer richiede una nuova conferma",
- "gatedStaleBody": "Il disclaimer della modalità Ricerca è stato aggiornato dall'ultima tua conferma. Rileggilo nelle impostazioni e conferma di nuovo per rivedere la curva.",
- "gatedCta": "Apri la modalità Ricerca nelle impostazioni",
"unknownDrug": "{name} non è nel catalogo GLP-1 approvato dall'EMA, quindi HealthLog non può tracciare una curva di farmacocinetica di popolazione."
}
},
@@ -1306,8 +1301,7 @@
"history": {
"back": "Torna al farmaco",
"subtitle": "Cronologia completa delle assunzioni",
- "plannedSuffix": "pianificato",
- "drugLevelDisclosure": "Mostra la curva stimata del principio attivo"
+ "plannedSuffix": "pianificato"
}
},
"logInjectionSiteTitle": "Registra il sito di iniezione",
diff --git a/messages/pl.json b/messages/pl.json
index b5cd296ad..1f43336f1 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -1127,11 +1127,6 @@
"emptyState": "Brak zarejestrowanych przyjęć.",
"emptyStateCta": "Zarejestruj dawkę, aby zobaczyć kształtującą się krzywą szacunkową.",
"estimateNote": "Edukacyjne oszacowanie z opublikowanej przez EMA farmakokinetyki populacyjnej. To nie jest pomiar.",
- "gatedTitle": "Tryb Badawczy wyłączony",
- "gatedBody": "Włącz tryb Badawczy w Ustawieniach, aby zobaczyć szacowaną krzywą poziomu leku. Krzywa jest opt-in, ponieważ to pomoc edukacyjna, a nie pomiar kliniczny.",
- "gatedStaleTitle": "Disclaimer wymaga ponownego potwierdzenia",
- "gatedStaleBody": "Disclaimer trybu Badawczego został zaktualizowany od czasu twojego ostatniego potwierdzenia. Przeczytaj go ponownie w Ustawieniach i potwierdź, aby krzywa znów się pojawiła.",
- "gatedCta": "Otwórz tryb Badawczy w Ustawieniach",
"unknownDrug": "{name} nie znajduje się w katalogu GLP-1 zatwierdzonym przez EMA, więc HealthLog nie może wyrysować krzywej farmakokinetyki populacyjnej."
}
},
@@ -1306,8 +1301,7 @@
"history": {
"back": "Powrót do leku",
"subtitle": "Pełna historia przyjęć",
- "plannedSuffix": "zaplanowane",
- "drugLevelDisclosure": "Pokaż szacowaną krzywą stężenia leku"
+ "plannedSuffix": "zaplanowane"
}
},
"logInjectionSiteTitle": "Zarejestruj miejsce iniekcji",
diff --git a/package.json b/package.json
index cee07cdbd..a9f9048de 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "healthlog",
- "version": "1.8.6",
+ "version": "1.8.7",
"description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.",
"license": "AGPL-3.0-only",
"homepage": "https://healthlog.dev",
diff --git a/src/app/insights/mood/page.tsx b/src/app/insights/mood/page.tsx
index 6e42bea95..a21ea684c 100644
--- a/src/app/insights/mood/page.tsx
+++ b/src/app/insights/mood/page.tsx
@@ -106,10 +106,9 @@ 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. */}
}
@@ -120,6 +119,10 @@ export default function InsightsStimmungPage() {
loading={isStatusLoading}
preparing={status?.preparing ?? false}
/>
+
+
+
+
);
}
diff --git a/src/app/medications/[id]/history/page.tsx b/src/app/medications/[id]/history/page.tsx
index e33edcab7..d934ad2e2 100644
--- a/src/app/medications/[id]/history/page.tsx
+++ b/src/app/medications/[id]/history/page.tsx
@@ -129,31 +129,22 @@ export default function IntakeHistoryPage({
/>
)}
- {/* O-2 — dose-strength (titration) curve + estimated
- active-ingredient curve as a default-CLOSED disclosure for
- GLP-1 medications. Genuine context, but opt-in so the intake
- table stays the dominant surface. The dose-strength curve plots
- the user's own logged dose-change history; the drug-level chart
- gates further on Research Mode. */}
+ {/* Estimated drug-level curve (the modelled active-ingredient
+ level) + dose-strength (titration) curve for GLP-1 medications.
+ Visible by default — the drug-level estimate is the one the
+ medication concerns, so it leads; the dose-strength curve plots
+ the user's own logged dose-change history below it. */}
{medication && isGlp1 && (
-
-
- {t("medications.detail.history.drugLevelDisclosure")}
-
-
-
-
-
-
+
+
+
+
)}
{importOpen && (
diff --git a/src/app/medications/[id]/page.tsx b/src/app/medications/[id]/page.tsx
index 32de81d9e..12c41cc55 100644
--- a/src/app/medications/[id]/page.tsx
+++ b/src/app/medications/[id]/page.tsx
@@ -235,30 +235,22 @@ export default function MedicationDetailPage({
(genuine history context). */}
{isGlp1 && }
- {/* GLP-1 dose-strength (titration) curve + estimated drug-level
- chart as a default-CLOSED disclosure — genuine history context,
- opt-in so the intake table stays the dominant surface. The
- dose-strength curve plots the user's own logged dose-change
- history; the drug-level chart gates further on Research Mode. */}
+ {/* GLP-1 estimated drug-level curve (the modelled active-ingredient
+ level) + dose-strength (titration) curve. Visible by default for
+ GLP-1 medications — the drug-level estimate is the one the
+ medication concerns, so it leads; the dose-strength curve plots
+ the user's own logged dose-change history below it. */}
{isGlp1 && (
-
-
- {t("medications.detail.history.drugLevelDisclosure")}
-
-
-
-
-
-
+
+
+
+
)}
);
diff --git a/src/components/charts/__tests__/trend-card.test.tsx b/src/components/charts/__tests__/trend-card.test.tsx
index a16977ba6..926480a78 100644
--- a/src/components/charts/__tests__/trend-card.test.tsx
+++ b/src/components/charts/__tests__/trend-card.test.tsx
@@ -123,6 +123,93 @@ describe(" directionSentiment", () => {
});
});
+describe(" value is never truncated", () => {
+ // v1.8.7 — the maintainer is emphatic: the numeric value must always
+ // render in full at any tile width (dense strip → narrowest column).
+ // The value span keeps its full intrinsic width (`shrink-0
+ // whitespace-nowrap`) and never carries `truncate`/`min-w-0`; when
+ // space is tight the UNIT yields instead. These guards pin the
+ // contract so a future layout churn can't reintroduce the clip.
+ function valueSpan(html: string): string {
+ const marker = 'data-slot="trend-card-value"';
+ const idx = html.indexOf(marker);
+ expect(idx).toBeGreaterThan(-1);
+ // Walk back to the opening "", idx);
+ return html.slice(open, close);
+ }
+
+ it("renders the full BP value (130) — never clipped to '13'", () => {
+ const html = render(
+ ,
+ );
+ // Value span renders the full reading (formatter adds one decimal).
+ expect(valueSpan(html)).toContain("130.0");
+ });
+
+ it("renders the full paired BP value (131/85)", () => {
+ const html = render(
+ ,
+ );
+ expect(valueSpan(html)).toContain("131.0/85.0");
+ });
+
+ it("does not put truncate or min-w-0 on the value node", () => {
+ const html = render(
+ ,
+ );
+ const value = valueSpan(html);
+ expect(value).not.toContain("truncate");
+ expect(value).not.toContain("min-w-0");
+ // The value holds its width and the number stays on one line.
+ expect(value).toContain("shrink-0");
+ expect(value).toContain("whitespace-nowrap");
+ });
+
+ it("lets the unit yield (min-w-0 truncate) instead of the value", () => {
+ const html = render(
+ ,
+ );
+ // The unit text renders in full in the markup; the truncate is a
+ // density safeguard that only engages when the column is too narrow.
+ expect(html).toContain("mL/(kg·min)");
+ });
+});
+
describe(" responsive layout", () => {
it("keeps long BP target tile content wrappable inside the card", () => {
// v1.4.28 FB-C2 — the BD-Zielbereich tile dropped `avgAllTime`
diff --git a/src/components/charts/trend-card.tsx b/src/components/charts/trend-card.tsx
index 84186ff47..cfc73b02a 100644
--- a/src/components/charts/trend-card.tsx
+++ b/src/components/charts/trend-card.tsx
@@ -238,7 +238,7 @@ export function TrendCard({
};
return (
-
+
{/* v1.4.25 W20a — single-line discipline + deterministic height for
the heading row so the value row below sits at the same baseline
across every tile. The label is locale-abbreviated upstream (see
@@ -265,21 +265,30 @@ export function TrendCard({
ask: "rechts neben der Zahl … der Pfeil [ist], wie gerade der
Trend ist". */}
+ {/* v1.4.28 R3c-Insights — `line-clamp-3` bounds caption
+ variance (FB-K2). A 1-sentence BP annotation paints
+ ~3 lines wrapped; a 4-line mood annotation used to push
+ the cell taller, which `auto-rows-fr` propagated to
+ every neighbour cell. With the clamp the longest
+ annotation ends with an ellipsis at 3 lines and the
+ row stays at a single visual rhythm. */}
+
+ {text}
+
+ {children}
+
+
+ );
+}
+
interface TrendAnnotationProps {
/** The metric this annotation describes. Drives the empty-state copy. */
metric: "bp" | "weight" | "mood";
@@ -122,36 +171,20 @@ export function TrendAnnotation({
}
return (
-
-
-
- {/* v1.4.28 R3c-Insights — `line-clamp-3` bounds caption
- variance (FB-K2). A 1-sentence BP annotation paints
- ~3 lines wrapped; a 4-line mood annotation used to push
- the cell taller, which `auto-rows-fr` propagated to
- every neighbour cell. With the clamp the longest
- annotation ends with an ellipsis at 3 lines and the
- row stays at a single visual rhythm. */}
-
+ {confidence ? (
+
+ {t(CONFIDENCE_LABEL_KEY[confidence])}
+
+ ) : null}
+
);
}
diff --git a/src/components/insights/trends-row.tsx b/src/components/insights/trends-row.tsx
index 77bc0e29d..1bfc94499 100644
--- a/src/components/insights/trends-row.tsx
+++ b/src/components/insights/trends-row.tsx
@@ -13,6 +13,7 @@ import {
} from "@/lib/insights/trend-chart-select";
import {
TrendAnnotation,
+ TrendCaptionCard,
type TrendAnnotationConfidenceBand,
type TrendAnnotationStatus,
} from "./trend-annotation";
@@ -216,13 +217,16 @@ export function TrendsRow({
// v1.8.6 W8 — additive metrics carry no advisor
// annotation. Paint the metric's standard one-line
// description so the card is never caption-less.
-
- {t(config.captionKey)}
-
+ // v1.8.7 W-E — render that description through the same
+ // `` shell the advisor caption uses, so
+ // the fallback caption shares the bordered card, Sparkles
+ // affordance, and `text-foreground` typography rather than
+ // standing out as a plain muted line.
+
)}
);
diff --git a/src/components/medications/DrugLevelChart.tsx b/src/components/medications/DrugLevelChart.tsx
index e414c2bbe..bc3d26c59 100644
--- a/src/components/medications/DrugLevelChart.tsx
+++ b/src/components/medications/DrugLevelChart.tsx
@@ -1,11 +1,13 @@
"use client";
/**
- * v1.4.25 W19c-Frontend — Estimated drug-level chart for GLP-1
- * medications. Opt-in behind Research Mode (the acknowledgment dialog
- * in this directory gates entry); even when enabled the chart paints
- * "Estimated level (relative)" on the y-axis without tick labels —
- * research §2.3 + W19c-Backend round report carry the rationale.
+ * Estimated drug-level chart for GLP-1 medications. Visible by default
+ * for any recognised GLP-1 drug — the curve is the modelled
+ * active-ingredient level the medication concerns. The y-axis paints
+ * "Estimated level (relative)" without tick labels and the persistent
+ * estimate disclaimer makes the pharmacokinetic-ESTIMATE framing
+ * explicit: this is a modelled value, not a measured blood
+ * concentration.
*
* Inputs (read-only):
* - Drug id resolved from the medication name (catalog lookup via
@@ -18,10 +20,6 @@
* applicable dose by walking the dose-change timeline; intakes
* that pre-date every dose-change row fall back to the
* medication's headline dose string.
- * - Research-mode state from `GET /api/auth/me/research-mode`. The
- * chart gates on `enabled === true && acknowledgedVersion ===
- * currentDisclaimerVersion` — defence-in-depth alongside the
- * server-side 400 the POST returns for stale versions.
*
* Pure math: `computeOneCompartment(drug, doses, asOf)` from
* `glp1-pk.ts`. The chart renders a 21-day window so three weekly
@@ -38,8 +36,7 @@
import { useMemo } from "react";
import { useQuery } from "@tanstack/react-query";
-import Link from "next/link";
-import { AlertTriangle, Loader2, Syringe } from "lucide-react";
+import { Loader2, Syringe } from "lucide-react";
import {
Area,
AreaChart,
@@ -50,7 +47,6 @@ import {
YAxis,
} from "recharts";
-import { Button } from "@/components/ui/button";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
import {
@@ -62,7 +58,6 @@ import {
type Glp1DrugId,
} from "@/lib/medications/glp1-knowledge";
import { parseDoseMg } from "@/lib/medications/dose-string";
-import type { ResearchModeStatus } from "@/lib/medications/research-mode-types";
import { prefersReducedMotion } from "@/lib/charts/reduced-motion";
import { MedicationDetailSection } from "@/components/medications/medication-detail-section";
@@ -128,23 +123,6 @@ export function DrugLevelChart({ medication, asOf }: DrugLevelChartProps) {
[medication.name],
);
- const { data: researchMode, isLoading: rmLoading } =
- useQuery({
- queryKey: queryKeys.researchMode(),
- queryFn: async () => {
- const res = await fetch("/api/auth/me/research-mode");
- if (!res.ok) return null;
- const json = await res.json();
- return json.data as ResearchModeStatus;
- },
- staleTime: 60 * 1000,
- });
-
- const versionsAligned =
- !!researchMode &&
- researchMode.acknowledgedVersion === researchMode.currentDisclaimerVersion;
- const gateOpen = !!researchMode?.enabled && versionsAligned;
-
const { data: details, isLoading: detailsLoading } =
useQuery({
queryKey: queryKeys.medicationGlp1Details(medication.id),
@@ -154,9 +132,7 @@ export function DrugLevelChart({ medication, asOf }: DrugLevelChartProps) {
const json = await res.json();
return json.data as Glp1DetailsResponse;
},
- // Only fetch when the user has opted in; saves a round-trip for
- // anyone who hasn't enabled Research Mode.
- enabled: gateOpen && !!drugId,
+ enabled: !!drugId,
staleTime: 60 * 1000,
});
@@ -170,7 +146,7 @@ export function DrugLevelChart({ medication, asOf }: DrugLevelChartProps) {
const json = await res.json();
return json.data as { events: IntakeEvent[] };
},
- enabled: gateOpen && !!drugId,
+ enabled: !!drugId,
staleTime: 60 * 1000,
});
@@ -194,7 +170,7 @@ export function DrugLevelChart({ medication, asOf }: DrugLevelChartProps) {
// ── Section header always renders so the parent's "Estimated drug
// level" anchor stays stable; the body switches between gated /
// loading / empty / chart states.
- const isLoading = rmLoading || (gateOpen && (detailsLoading || intakeLoading));
+ const isLoading = !!drugId && (detailsLoading || intakeLoading);
const hasDoses = doses.length > 0;
const animationsEnabled = !prefersReducedMotion();
// Single-window contract — three weekly cycles with the sample step
@@ -214,19 +190,12 @@ export function DrugLevelChart({ medication, asOf }: DrugLevelChartProps) {
) : null;
// Decision tree:
- // 1. drug not in catalog → gated-unknown-drug placeholder
- // 2. !researchMode.enabled OR → gated placeholder + Settings CTA
- // versions !== aligned (the defence-in-depth rule)
- // 3. loading → skeleton
- // 4. no intake events → empty-state with log CTA
- // 5. otherwise → AreaChart
+ // 1. drug not in catalog → unknown-drug placeholder
+ // 2. loading → skeleton
+ // 3. no intake events → empty-state with log CTA
+ // 4. otherwise → AreaChart
const body = !drugId ? (
- ) : !gateOpen ? (
-
) : isLoading ? (
// v1.4.43 W11-L6 — match the loaded chart height (240 px) so the
// dashboard tile doesn't reflow when the levels render.
@@ -257,9 +226,12 @@ export function DrugLevelChart({ medication, asOf }: DrugLevelChartProps) {
dataSlot="drug-level-chart"
>
{body}
- {gateOpen && hasDoses && (
+ {/* The pharmacokinetic-ESTIMATE disclaimer stays attached whenever
+ a recognised GLP-1 drug renders the chart — the curve is a
+ modelled value, never a measured blood concentration. */}
+ {!!drugId && (
{t("medications.researchMode.chart.estimateNote")}
@@ -306,52 +278,6 @@ function GatedUnknownDrug({ medicationName }: { medicationName: string }) {
);
}
-function GatedPlaceholder({
- versionsAligned,
- knownState,
-}: {
- versionsAligned: boolean;
- knownState: ResearchModeStatus | null | undefined;
-}) {
- const { t } = useTranslations();
- // Three distinguishable states:
- // - Research Mode is off entirely.
- // - Research Mode is on but the acknowledged version is stale.
- // - We don't know yet (server returned null) — still render the
- // opt-in CTA so the user has a path forward.
- const stale = !!knownState?.enabled && !versionsAligned;
- return (
-