mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
Merge pull request #464 from MBombeck/release/v1.28.20
release/v1.28.20 — estimated drug level in efficacy + insights
This commit is contained in:
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