mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 06:40:50 +00:00
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:
1 parent
de0689e419
commit
17b8d8d2c2
528 files changed
+104033
-6524
No files matched your search
@@ -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 };
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user