From 349cfa4d8085dc742f22933af2004c7dda5a3aae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 10 Jul 2026 14:33:47 +0200 Subject: [PATCH] =?UTF-8?q?chore(release):=20v1.28.20=20=E2=80=94=20estima?= =?UTF-8?q?ted=20drug=20level=20in=20efficacy=20+=20insights?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The GLP-1 estimated active-substance curve (the modelled drug level from logged doses) now also renders in the efficacy (Wirkung) tab and in the Insights medication overview, not only under the injection tab. Both reuse the same self-contained DrugLevelChart, gated on the GLP-1 treatment class, so the drug level sits next to the efficacy readouts. Additive — no existing surface changes; non-GLP-1 medications render nothing. --- CHANGELOG.md | 8 ++ docs/api/openapi.yaml | 2 +- package.json | 2 +- public/sw.js | 2 +- src/app/insights/medications/page.tsx | 11 +++ .../insights/glp1-substance-curve-summary.tsx | 85 +++++++++++++++++++ .../detail/efficacy/efficacy-tab.tsx | 45 ++++++++++ .../detail/medication-detail-tabs.tsx | 8 +- 8 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 src/components/insights/glp1-substance-curve-summary.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fded7059..19de0f73c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## [Unreleased] +## [1.28.20] — 2026-07-10 — Estimated drug level where you read efficacy + +The estimated active-substance curve for a GLP-1 medication — the modelled drug +level from your logged doses — now also appears in the efficacy view and in the +Insights medication overview, not only under the injection tab. It is the same +estimate shown there, surfaced next to the efficacy readouts so the drug level +and its effect sit side by side. Non-GLP-1 medications are unaffected. + ## [1.28.19] — 2026-07-10 — Document and card polish Small refinements across a few surfaces. diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 78916a428..b735a4920 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.28.19 + version: 1.28.20 description: >- Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest. diff --git a/package.json b/package.json index f14c617f3..8851553e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "healthlog", - "version": "1.28.19", + "version": "1.28.20", "description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.", "license": "PolyForm-Noncommercial-1.0.0", "homepage": "https://healthlog.dev", diff --git a/public/sw.js b/public/sw.js index 4eb8b44e0..d2129c86a 100644 --- a/public/sw.js +++ b/public/sw.js @@ -36,7 +36,7 @@ try { // v1.4.38.4 → v1.4.42. Do not hand-edit; bump `package.json` and rebuild. const CACHE_VERSION = (typeof self !== "undefined" && self.__APP_VERSION__) || - /* @sw-version-fallback */ "v1.28.19"; + /* @sw-version-fallback */ "v1.28.20"; const STATIC_CACHE = `healthlog-static-${CACHE_VERSION}`; const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`; // v1.18.6 — read-only data cache for a curated allowlist of safe GET `/api/*` diff --git a/src/app/insights/medications/page.tsx b/src/app/insights/medications/page.tsx index fc8eade3a..552b5eaaf 100644 --- a/src/app/insights/medications/page.tsx +++ b/src/app/insights/medications/page.tsx @@ -18,6 +18,7 @@ import { CoachLaunchButton } from "@/components/insights/coach-launch-button"; import { InsightStatusCard } from "@/components/insights/insight-status-card"; import { MetricTargetSummary } from "@/components/insights/metric-target-summary"; import { MedicationEfficacySummary } from "@/components/insights/medication-efficacy-summary"; +import { Glp1SubstanceCurveSummary } from "@/components/insights/glp1-substance-curve-summary"; import { SubPageShell } from "@/components/insights/sub-page-shell"; import { TileHeader } from "@/components/insights/tile-header"; import { apiGet } from "@/lib/api/api-fetch"; @@ -288,6 +289,16 @@ export default function InsightsMedikamentePage() { medications={medications.map((m) => ({ id: m.id, name: m.name }))} /> + {/* v1.28.20 — estimated active-substance curve for GLP-1 medications + (additive; renders nothing when the user has no GLP-1 medication). */} + ({ + id: m.id, + name: m.name, + dose: m.dose, + }))} + /> + {/* v1.12.2 — the assessment is the LAST block on every bespoke diff --git a/src/components/insights/glp1-substance-curve-summary.tsx b/src/components/insights/glp1-substance-curve-summary.tsx new file mode 100644 index 000000000..9b337043d --- /dev/null +++ b/src/components/insights/glp1-substance-curve-summary.tsx @@ -0,0 +1,85 @@ +"use client"; + +/** + * v1.28.20 — compact GLP-1 estimated active-substance curve for the Insights + * medication surface. Additive to the medication detail page's Injektion and + * Wirkung tabs: the same self-contained `DrugLevelChart` (modelled + * one-compartment level from the user's logged intakes + dose changes), + * surfaced where the compliance overview already lives. + * + * Gating: reads `GET /api/insights/glp1-timeline` — the server-authoritative + * GLP-1 signal. When `hasGlp1` is false the block renders nothing. The set of + * GLP-1 medication names comes from the timeline's entries (every entry is + * from a `treatmentClass === "GLP1"` medication), and the passed medication + * list is filtered to that set so non-GLP-1 medications never render a curve. + * + * The chart carries its own `MedicationDetailSection` card chrome + estimate + * disclaimer, so it reads as one system with the surrounding Insights cards + * without a second computation. Recharts loads through `next/dynamic` + * (client-only) to stay out of the page's initial bundle. + */ +import dynamic from "next/dynamic"; +import { useQuery } from "@tanstack/react-query"; + +import { ChartSkeleton } from "@/components/charts/chart-skeleton"; +import { queryKeys } from "@/lib/query-keys"; +import { apiGet } from "@/lib/api/api-fetch"; + +const DrugLevelChart = dynamic( + () => + import("@/components/charts/chart-runtime").then((mod) => ({ + default: mod.DrugLevelChart, + })), + { ssr: false, loading: () => }, +); + +interface Glp1TimelineResponse { + hasGlp1: boolean; + entries: { medicationName?: string }[]; +} + +export function Glp1SubstanceCurveSummary({ + medications, +}: { + medications: { id: string; name: string; dose: string }[]; +}) { + const { data } = useQuery({ + queryKey: queryKeys.insightsGlp1Timeline(200), + queryFn: () => + apiGet("/api/insights/glp1-timeline?limit=200"), + staleTime: 60_000, + }); + + if (!data?.hasGlp1) return null; + + // Every timeline entry is from a GLP-1 medication (server filters on + // `treatmentClass: "GLP1"`). Medication names match the comprehensive + // list exactly — both derive from `medication.name` server-side. + const glp1Names = new Set( + data.entries + .map((e) => e.medicationName) + .filter((n): n is string => typeof n === "string" && n.length > 0), + ); + + const glp1Meds = medications.filter((m) => glp1Names.has(m.name)); + if (glp1Meds.length === 0) return null; + + const showNameLabel = glp1Meds.length > 1; + + return ( +
+ {glp1Meds.map((med) => ( +
+ {showNameLabel ? ( +

+ {med.name} +

+ ) : null} + +
+ ))} +
+ ); +} diff --git a/src/components/medications/detail/efficacy/efficacy-tab.tsx b/src/components/medications/detail/efficacy/efficacy-tab.tsx index 2d465ecca..10914ecea 100644 --- a/src/components/medications/detail/efficacy/efficacy-tab.tsx +++ b/src/components/medications/detail/efficacy/efficacy-tab.tsx @@ -44,12 +44,39 @@ const EfficacyChart = dynamic( { ssr: false, loading: () => }, ); +// v1.28.20 — the estimated active-substance curve (Wirkstoffverlauf) also +// surfaces here, additive to the Injektion tab. Same self-contained chart, +// same `next/dynamic` (client-only) load that keeps Recharts out of the +// initial bundle. +const DrugLevelChart = dynamic( + () => + import("@/components/charts/chart-runtime").then((mod) => ({ + default: mod.DrugLevelChart, + })), + { ssr: false, loading: () => }, +); + export function EfficacyTab({ medicationId, active, + isGlp1 = false, + medicationName, + medicationDose, }: { medicationId: string; active: boolean; + /** + * True when the parent already knows the medication is a (non-one-shot) + * GLP-1 drug (`treatmentClass === "GLP1"`). Threaded from the detail-tabs + * parent because the efficacy DTO carries the target class, not the raw + * treatment class. Gates the estimated active-substance curve exactly the + * way the Injektion tab does — no Research-Mode wrapper, GLP-1 alone. + */ + isGlp1?: boolean; + /** Brand/generic name the substance-curve chart resolves to a catalog id. */ + medicationName?: string; + /** Headline dose string, the chart's per-intake dose fallback. */ + medicationDose?: string; }) { const { t } = useTranslations(); const { user } = useAuth(); @@ -136,6 +163,24 @@ export function EfficacyTab({ /> ))} + {/* v1.28.20 — estimated active-substance curve for GLP-1 medications, + mirroring the Injektion tab. The chart is self-contained (fetches + its own intake + dose-change data and renders its own + `MedicationDetailSection` card + estimate disclaimer); it slots + straight into the tab's `space-y-6` rhythm. Additive: no existing + Wirkung content changes. */} + {isGlp1 && medicationName != null && medicationDose != null ? ( +
+ +
+ ) : null} +