Merge v1.25.1: deep data/cross-cut (cadence-aware alert compliance, lab-unit guard, signals in briefing, biomarker fallback, cleanups)

This commit is contained in:
Marc-André Bombeck committed 2026-06-29 00:38:45 +02:00
commit 2ec4828c1f
21 files changed
+509 -80

No files matched your search

+2 -10
View File
@@ -15,23 +15,15 @@ import { useTranslations } from "@/lib/i18n/context";
* The copy lives under the `selfDescription` i18n namespace so every locale
* carries the same set; pick the note that fits the surface:
*
* - `intendedPurpose` — the canonical "what this is / is not, not a medical
* device" line. Reuse verbatim; do not paraphrase per surface.
* - `coach` — the Coach standing line (works from your data, not a doctor).
* - `emergency` — the red-flag interstitial note (contact emergency services).
* - `dataPosture` — self-hosted, data stays on your instance, BYOK / local AI.
*
* The acknowledged onboarding disclaimer (`onboarding.disclaimer.*`) and the
* legal medical-device boundary on the public privacy page remain the standing
* floor; these notes are the gentle, in-context layer above it.
* floor; this note is the gentle, in-context layer above it.
*/
export type DisclaimerVariant =
"intendedPurpose" | "coach" | "emergency" | "dataPosture";
export type DisclaimerVariant = "dataPosture";
const VARIANT_KEY: Record<DisclaimerVariant, string> = {
intendedPurpose: "selfDescription.intendedPurpose",
coach: "selfDescription.coach",
emergency: "selfDescription.emergency",
dataPosture: "selfDescription.dataPosture",
};
+54 -20
View File
@@ -111,7 +111,11 @@ export function LabBiomarkerDetail({ biomarkerId }: { biomarkerId: string }) {
const [lowerInput, setLowerInput] = useState("");
const [upperInput, setUpperInput] = useState("");
const { data: marker, isError: markerError } = useQuery({
const {
data: marker,
isError: markerError,
refetch: refetchMarker,
} = useQuery({
queryKey: queryKeys.biomarkerDetail(biomarkerId),
queryFn: () => apiGet<BiomarkerDto>(`/api/biomarkers/${biomarkerId}`),
});
@@ -125,6 +129,7 @@ export function LabBiomarkerDetail({ biomarkerId }: { biomarkerId: string }) {
data: list,
isLoading,
isError: listError,
refetch: refetchList,
} = useInfiniteQuery({
queryKey: queryKeys.labResultsInfinite({ biomarkerId, sortDir: "desc" }),
initialPageParam: 0,
@@ -239,11 +244,50 @@ export function LabBiomarkerDetail({ biomarkerId }: { biomarkerId: string }) {
onError: () => toast.error(t("labs.biomarker.targetRange.saveError")),
});
// v1.25.1 — the per-marker description leads the page, mirroring the metric
// sub-pages' explainer caption. Resolve the catalog slug from the marker name
// and fall back to the user's own `context`, then to a generic explainer so a
// catalog-less custom marker never renders an empty description block (parity
// with the insights metric pages, which always carry one).
const catalogSlug = marker?.name
? slugByName.get(marker.name.trim().toLowerCase())
: undefined;
const description = catalogSlug
? t(`labs.catalog.desc.${catalogSlug}`)
: marker?.context?.trim()
? marker.context.trim()
: t("labs.detail.genericDescription");
// v1.25.1 (H2-M2) — the error state keeps the heading + description and adds a
// retry, mirroring the insights `SubPageShell` recovery affordance rather than
// collapsing to a bare line with no context and no way back.
if (markerError || listError) {
return (
<p className="text-destructive py-8 text-center text-sm">
{t("labs.loadError")}
</p>
<div className="space-y-6">
<header className="space-y-1.5">
<h1 className="text-2xl font-bold tracking-tight">
{marker?.name ?? t("labs.detail.title")}
</h1>
<p className="text-muted-foreground text-sm leading-relaxed">
{description}
</p>
</header>
<Card>
<CardContent className="flex flex-col items-center gap-3 py-8 text-center">
<p className="text-destructive text-sm">{t("labs.loadError")}</p>
<Button
variant="outline"
size="sm"
onClick={() => {
if (markerError) void refetchMarker();
if (listError) void refetchList();
}}
>
{t("common.retry")}
</Button>
</CardContent>
</Card>
</div>
);
}
@@ -330,22 +374,12 @@ export function LabBiomarkerDetail({ biomarkerId }: { biomarkerId: string }) {
</div>
{/* v1.25.1 — the static description leads the page, directly beneath the
heading, mirroring the metric sub-pages' explainer caption. Resolves
the catalog slug from the marker name, falling back to the user's own
`context`. */}
{(() => {
const slug = marker?.name
? slugByName.get(marker.name.trim().toLowerCase())
: undefined;
const description = slug
? t(`labs.catalog.desc.${slug}`)
: (marker?.context ?? null);
return description ? (
<p className="text-muted-foreground text-sm leading-relaxed">
{description}
</p>
) : null;
})()}
heading, mirroring the metric sub-pages' explainer caption. Computed
once above (catalog desc → user `context` → generic fallback) so a
catalog-less marker still carries a description. */}
<p className="text-muted-foreground text-sm leading-relaxed">
{description}
</p>
{isLoading ? (
<Card>
@@ -703,6 +703,7 @@ export function MedicationDetailTabs({
label={t("medications.detail.erweitert.group.externalApi")}
dataSlot="api-group-external-api"
collapsible
defaultOpen={false}
>
<div className="py-3">
<ApiTokensRow
@@ -727,6 +728,7 @@ export function MedicationDetailTabs({
label={t("medications.detail.erweitert.group.lifecycle")}
dataSlot="erweitert-group-lifecycle"
collapsible
defaultOpen={false}
>
<div className="py-3">
<LifecycleManageBody
@@ -301,7 +301,10 @@ export function MentalWellbeing() {
{result.crisis && (
<Card
className="border-amber-400/50"
// v1.25.1 — calm destructive-subtle border instead of the amber
// tint v1.25 retired; the role/aria-live carry the urgency, the
// tone stays consistent with the rest of the app.
className="border-destructive/40"
role="alert"
aria-live="assertive"
>