mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
chore(release): v1.28.20 — estimated drug level in efficacy + insights
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.
This commit is contained in:
1 parent
5f558abc33
commit
349cfa4d80
8 files changed
+159
-4
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
+1
-1
@@ -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/*`
|
||||
|
||||
@@ -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). */}
|
||||
<Glp1SubstanceCurveSummary
|
||||
medications={medications.map((m) => ({
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
dose: m.dose,
|
||||
}))}
|
||||
/>
|
||||
|
||||
<MetricTargetSummary slug="medications" />
|
||||
|
||||
{/* v1.12.2 — the assessment is the LAST block on every bespoke
|
||||
|
||||
@@ -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: () => <ChartSkeleton /> },
|
||||
);
|
||||
|
||||
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<Glp1TimelineResponse>("/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 (
|
||||
<div className="space-y-4" data-slot="insights-glp1-substance-curve">
|
||||
{glp1Meds.map((med) => (
|
||||
<div key={med.id} className="space-y-2">
|
||||
{showNameLabel ? (
|
||||
<p className="text-muted-foreground px-1 text-xs font-medium">
|
||||
{med.name}
|
||||
</p>
|
||||
) : null}
|
||||
<DrugLevelChart
|
||||
medication={{ id: med.id, name: med.name, dose: med.dose }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -44,12 +44,39 @@ const EfficacyChart = dynamic(
|
||||
{ ssr: false, loading: () => <ChartSkeleton /> },
|
||||
);
|
||||
|
||||
// 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: () => <ChartSkeleton /> },
|
||||
);
|
||||
|
||||
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 ? (
|
||||
<div data-slot="wirkung-drug-level-section">
|
||||
<DrugLevelChart
|
||||
medication={{
|
||||
id: medicationId,
|
||||
name: medicationName,
|
||||
dose: medicationDose,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<p
|
||||
className="text-muted-foreground text-xs"
|
||||
data-slot="medication-wirkung-disclaimer"
|
||||
|
||||
@@ -560,7 +560,13 @@ export function MedicationDetailTabs({
|
||||
descriptive; hidden for one-shot + no-target medications. */}
|
||||
{hasEfficacyTarget && (
|
||||
<TabsContent value="wirkung" className="space-y-6 pt-2">
|
||||
<EfficacyTab medicationId={id} active={activeTab === "wirkung"} />
|
||||
<EfficacyTab
|
||||
medicationId={id}
|
||||
active={activeTab === "wirkung"}
|
||||
isGlp1={isGlp1}
|
||||
medicationName={medication.name}
|
||||
medicationDose={medication.dose}
|
||||
/>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
|
||||
Reference in new issue
Block a user