chore(release): v1.4.25

Largest feature delta in the v1.4.x line. Insights expands into seven dedicated metric sub-pages; GLP-1 medication tracking lands end-to-end (drug-level chart with Research Mode acknowledgment, EMA titration ladder, cadence + compliance, side-effect taxonomy, pen/vial inventory); cross-source priority becomes a two-axis resolver; per-user timezone threads through ten analytics surfaces; Withings activity + sleep v2 sync; onboarding rebuilt as a nested-route wizard; Personal Records ship end-to-end; Health Score provenance accordion; Coach runs native first-party prompts across six locales with a 1800-assertion refusal-probe matrix; OpenAPI drift gate hard-fails; multi-arch GHCR image. Migrations 0043 through 0060 are additive and forward-only. PROMPT_VERSION 4.24.0 → 4.25.0 with GROUND RULE 9 (refuses dose recommendations) and GROUND RULE 15 (refuses drug-level estimates with MDR + MDCG 2021-24 cites).

See CHANGELOG.md for the full per-section delta.
This commit is contained in:
Marc-André Bombeck authored and GitHub committed 2026-05-14 23:49:32 +02:00
1 parent de0689e419
commit 17b8d8d2c2
528 files changed
+104033 -6524

No files matched your search

+52
View File
@@ -0,0 +1,52 @@
"use client";
import { useCallback, useState } from "react";
import type { CoachScope } from "@/lib/ai/coach/types";
/**
* v1.4.25 W3e — controlled state for the Coach drawer when mounted
* outside the `/insights` page (currently `/targets`, future
* `/insights/<metric>` sub-pages).
*
* Encapsulates the open / prefill / scope triple so each new surface
* only has to mount `<CoachDrawer>` and wire the page-level CTAs to
* the returned `askCoach()` callback. The drawer itself owns no
* cross-surface state; this hook is the binding point.
*
* Reset behaviour mirrors the v1.4.20 Insights pattern: closing the
* drawer clears prefill so the next open starts blank unless a CTA
* supplies a fresh prefill.
*/
export interface CoachHandoffApi {
coachOpen: boolean;
setCoachOpen: (next: boolean) => void;
coachPrefill: string | null;
coachScope: CoachScope | null;
askCoach: (payload: { prefill: string; scope: CoachScope }) => void;
}
export function useCoachHandoff(): CoachHandoffApi {
const [coachOpen, setOpen] = useState(false);
const [coachPrefill, setPrefill] = useState<string | null>(null);
const [coachScope, setScope] = useState<CoachScope | null>(null);
const setCoachOpen = useCallback((next: boolean) => {
setOpen(next);
if (!next) {
setPrefill(null);
setScope(null);
}
}, []);
const askCoach = useCallback(
({ prefill, scope }: { prefill: string; scope: CoachScope }) => {
setPrefill(prefill);
setScope(scope);
setOpen(true);
},
[],
);
return { coachOpen, setCoachOpen, coachPrefill, coachScope, askCoach };
}
+82
View File
@@ -0,0 +1,82 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { useAuth } from "@/hooks/use-auth";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
/**
* Shape returned by every `/api/insights/<metric>-status` endpoint.
*
* - `hasProvider` — whether the user has configured an AI provider; the
* sub-page hides the status card entirely when false.
* - `text` — the rendered status string; null when the provider has
* not yet produced one.
* - `cached` — whether the payload came from the per-locale cache or a
* fresh provider round-trip.
* - `updatedAt` — ISO timestamp of the cached payload's creation, or
* null when the route has never produced a value.
*/
export interface InsightStatusData {
hasProvider: boolean;
text: string | null;
cached: boolean;
updatedAt: string | null;
}
/**
* Metric slugs the sub-pages render. Each slug is paired with the
* Insights-status endpoint and the matching `queryKeys.*` factory so
* the cache keys stay aligned across the whole app — a hard-coded
* `["insights", "weight-status", locale]` array typoed once is exactly
* the class of bug `queryKeys` was introduced to defend against.
*/
type InsightStatusMetric =
| "blood-pressure"
| "weight"
| "pulse"
| "bmi"
| "mood"
| "medication-compliance";
const QUERY_KEY_FACTORY: Record<
InsightStatusMetric,
(locale: string) => readonly unknown[]
> = {
"blood-pressure": queryKeys.insightsBpStatus,
weight: queryKeys.insightsWeightStatus,
pulse: queryKeys.insightsPulseStatus,
bmi: queryKeys.insightsBmiStatus,
mood: queryKeys.insightsMoodStatus,
"medication-compliance": queryKeys.insightsMedicationComplianceStatus,
};
/**
* Shared loader for the insight-status payload backing
* `/insights/<metric>` sub-pages. Five sub-pages previously copy-pasted
* the same 13-line useQuery block plus an identical `XxxStatusData`
* interface; the hook collapses them onto one helper plus the existing
* `queryKeys` factory so the next "add a metric" PR touches one map
* entry instead of an entire page module.
*
* Returns the `useQuery` result narrowed to the `InsightStatusData`
* payload — callers stay free to use `data` / `isLoading` / refetch
* helpers without an extra unwrap.
*/
export function useInsightStatus(metric: InsightStatusMetric) {
const { isAuthenticated } = useAuth();
const { locale } = useTranslations();
return useQuery({
queryKey: QUERY_KEY_FACTORY[metric](locale),
queryFn: async (): Promise<InsightStatusData> => {
const res = await fetch(`/api/insights/${metric}-status?locale=${locale}`);
if (!res.ok) throw new Error("Failed");
const json = (await res.json()) as { data: InsightStatusData };
return json.data;
},
enabled: isAuthenticated,
staleTime: 60 * 1000,
});
}
+42
View File
@@ -0,0 +1,42 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { queryKeys } from "@/lib/query-keys";
import {
resolveDashboardLayout,
type DashboardLayout,
} from "@/lib/dashboard-layout";
/**
* v1.4.25 W4 — shared reader for the dashboard-layout payload across
* the insights surfaces.
*
* The mother page and every sub-page need the persisted
* `comparisonBaseline` so the dashboard / insights chart-cog stays
* in sync. The shape repeats the same `useQuery({ … queryKey:
* queryKeys.dashboardWidgets() … })` four-line block that lived inline
* on the mother page — extracted here so a new sub-page consumer drops
* the boilerplate and so the TanStack-Query cache lookup stays
* authoritative for every consumer.
*
* Returns the resolved `comparisonBaseline` (defaults to `"none"`)
* directly so consumers don't have to remember the `resolveDashboardLayout`
* fallback dance. `enabled` controls whether the query fires — the
* sub-pages gate this on `useAuth().isAuthenticated`.
*/
export function useInsightsLayoutPrefs(enabled: boolean) {
const { data } = useQuery({
queryKey: queryKeys.dashboardWidgets(),
queryFn: async () => {
const res = await fetch("/api/dashboard/widgets");
if (!res.ok) throw new Error("Failed to load dashboard layout");
const json = await res.json();
return json.data as DashboardLayout;
},
enabled,
});
const compareBaseline =
resolveDashboardLayout(data).comparisonBaseline ?? "none";
return { layout: data ?? null, compareBaseline } as const;
}