feat(self-description): add the non-diagnostic / intended-purpose standard

Introduce one canonical "what this is / is not, not a medical device"
statement plus a layered, calm disclaimer set, and apply it on the
surfaces that earn a context note.

- New shared `MedicalDisclaimer` component renders the copy as plain
  React text (no markdown renderer, per the XSS posture). Variants:
  intendedPurpose, coach, insights, labs, medications, emergency,
  dataPosture.
- New `selfDescription` i18n namespace across all six locales: the
  intended-purpose line, the per-surface notes, the emergency note, and
  an explicit data-posture statement (self-hosted, data stays on your
  instance, encrypted at rest, BYOK / local model, no third-party
  analytics).
- Apply once per surface, layered not plastered: a calm standing line on
  the Coach new-chat hero, one framing line in the Insights layout shell
  (covers the briefing and trends), a labs note (ranges vary, only a
  clinician interprets), a neutral medications note, and the data-posture
  line where the provider key is configured. Mental-wellbeing keeps its
  existing crisis copy; records keep their existing note.
- Extend the Coach system prompt (EN + DE) with a self-description ground
  rule so the Coach answers "what are you / where does my data go"
  honestly and stays non-diagnostic.

App-authored copy was already observational; the few diagnose/treat
tokens are negations, idioms, or framed as the user's own self-reported
statement, so no softening was required.
This commit is contained in:
Marc-André Bombeck committed 2026-06-28 17:43:31 +02:00
1 parent a27a1ee71d
commit 3bd52a322b
13 files changed
+195 -5

No files matched your search

+6
View File
@@ -6,6 +6,7 @@ import { useRouter } from "next/navigation";
import { useQueryClient } from "@tanstack/react-query";
import { Loader2, Pencil, Plus, ScanLine, Wrench } from "lucide-react";
import { MedicalDisclaimer } from "@/components/common/medical-disclaimer";
import { LabForm } from "@/components/labs/lab-form";
import { LabList } from "@/components/labs/lab-list";
import { OcrReviewDialog } from "@/components/labs/ocr-review-dialog";
@@ -173,6 +174,11 @@ export default function LabsPage() {
) : null}
<LabList onAddFirst={() => setDialogOpen(true)} />
{/* v1.25 — calm, context-specific framing for lab values: your own
records, ranges vary by lab and person, only a clinician interprets
them. One footer line, not a per-row banner. */}
<MedicalDisclaimer variant="labs" className="px-1 pt-2" />
</div>
);
}
+7
View File
@@ -7,6 +7,7 @@ import { useRouter, useSearchParams } from "next/navigation";
import { useAuth } from "@/hooks/use-auth";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
import { MedicalDisclaimer } from "@/components/common/medical-disclaimer";
import { MedicationWizardDialog } from "@/components/medications/wizard/medication-wizard-dialog";
import { MedicationCard } from "@/components/medications/medication-card";
import { Glp1MedicationCard } from "@/components/medications/glp1-medication-card";
@@ -524,6 +525,12 @@ export default function MedicationsPage() {
</div>
)}
{/* v1.25 — calm framing for the medications surface: this tracks what
you log and your own adherence, it doesn't assess dosing or
interactions and isn't medical advice. One footer line, neutral —
the med cards themselves stay un-tinted. */}
<MedicalDisclaimer variant="medications" className="px-1 pt-2" />
{/* v1.5.5 — the per-card kebab triggers used to open inline
IntakeImportDialog / ApiEndpointDialog mounts here, but both
state slots were only ever set from inside the dialogs
@@ -0,0 +1,75 @@
"use client";
import { cn } from "@/lib/utils";
import { useTranslations } from "@/lib/i18n/context";
/**
* v1.25 — the one place the app states what it is and is not.
*
* HealthLog's self-description standard is layered, not a wall of warnings:
* a single intended-purpose statement plus calm, context-specific notes that
* sit once on the surface they belong to. Every string is plain React text —
* no markdown renderer, in line with the project's XSS posture (Coach,
* briefing and insights all render as text children).
*
* 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).
* - `insights` — descriptive-patterns framing for insights + the briefing.
* - `labs` — your own records, ranges vary, only a clinician interprets.
* - `medications` — tracks what you log, not dosing/interaction advice.
* - `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.
*/
export type DisclaimerVariant =
| "intendedPurpose"
| "coach"
| "insights"
| "labs"
| "medications"
| "emergency"
| "dataPosture";
const VARIANT_KEY: Record<DisclaimerVariant, string> = {
intendedPurpose: "selfDescription.intendedPurpose",
coach: "selfDescription.coach",
insights: "selfDescription.insights",
labs: "selfDescription.labs",
medications: "selfDescription.medications",
emergency: "selfDescription.emergency",
dataPosture: "selfDescription.dataPosture",
};
export interface MedicalDisclaimerProps {
variant: DisclaimerVariant;
className?: string;
}
/**
* A calm, muted, plain-text disclaimer line. Defaults to a small,
* unobtrusive footnote treatment; pass `className` to tune spacing for the
* surface. Keep it to one instance per surface — the standard is layered,
* not repeated.
*/
export function MedicalDisclaimer({
variant,
className,
}: MedicalDisclaimerProps) {
const { t } = useTranslations();
return (
<p
data-slot="medical-disclaimer"
data-variant={variant}
className={cn("text-muted-foreground text-xs leading-relaxed", className)}
>
{t(VARIANT_KEY[variant])}
</p>
);
}
@@ -2,6 +2,7 @@
import { Sparkles } from "lucide-react";
import { MedicalDisclaimer } from "@/components/common/medical-disclaimer";
import { cn } from "@/lib/utils";
import { useTranslations } from "@/lib/i18n/context";
@@ -121,6 +122,15 @@ export function CoachHero({ composer, scopeHint }: CoachHeroProps) {
{scopeHint}
</div>
) : null}
{/* v1.25 — the Coach standing self-description: works from your own
data, not a doctor, not medical advice. Sits once on the calm
new-chat surface so it frames the conversation without nagging
every reply. */}
<MedicalDisclaimer
variant="coach"
className="max-w-md text-center text-pretty"
/>
</div>
</div>
);
@@ -8,6 +8,7 @@ import { useMounted } from "@/hooks/use-mounted";
import { useFeatureFlags } from "@/hooks/use-feature-flags";
import { useWorkouts } from "@/hooks/use-workouts";
import { InsightsTabStrip } from "@/components/insights/insights-tab-strip";
import { MedicalDisclaimer } from "@/components/common/medical-disclaimer";
import {
nextAdvisorPollInterval,
useInsightsAdvisorQuery,
@@ -159,11 +160,14 @@ export function InsightsLayoutShell({ children }: { children: ReactNode }) {
modules={user?.modules}
/>
{children}
{/* v1.18.6 (DISC-01) — the page-level generic medical disclaimer is
removed. The "HealthLog is a private tracking tool, not a clinical
assessment or diagnosis" acknowledgment now lives once at the start
of onboarding, and the legal text stays reachable on the public
privacy page. */}
{/* v1.18.6 (DISC-01) — the page-level generic medical disclaimer was
removed; the standing acknowledgment lives once at the start of
onboarding and the legal text on the public privacy page.
v1.25 — one calm, context-specific framing line for the whole
Insights surface (the briefing, trends and correlations included):
these are descriptive patterns from the user's own data, not a
clinical assessment. Layered, not a wall of warnings. */}
<MedicalDisclaimer variant="insights" className="px-1" />
</div>
);
}
@@ -19,6 +19,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Sparkles } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { MedicalDisclaimer } from "@/components/common/medical-disclaimer";
import { Label } from "@/components/ui/label";
import { NativeSelect } from "@/components/ui/native-select";
import { SettingsCard } from "@/components/settings/settings-card";
@@ -201,6 +202,11 @@ export function AiInsightsCard({
})
}
/>
{/* v1.25 — the data-posture statement lives where the provider key is
configured: self-hosted, data stays on this instance, BYOK / local
model, no third-party analytics. One calm line, not a banner. */}
<MedicalDisclaimer variant="dataPosture" />
</div>
</SettingsCard>
);
+28
View File
@@ -285,6 +285,19 @@ GROUND RULES
persuasion for its own sake. If a nudge would only serve to push, drop it
and reflect instead. You inform and support a decision that stays theirs;
you never manipulate it.
21. Describe HealthLog honestly when asked what you are, what this app is,
or where the user's data goes. HealthLog is a self-hosted personal
health record-keeping and wellness tool — it helps the user log,
visualise and export their OWN data. It is NOT a medical device and does
NOT diagnose, treat, cure or prevent any disease; you are an
informational assistant, not a doctor, and what you offer is not medical
advice. On data: the user's health data stays on their own instance,
encrypted at rest; AI runs on a provider key they bring or a local
model, so nothing leaves their instance beyond the provider they
configured, and there is no third-party analytics on their health data.
State this plainly and calmly when relevant; never oversell it as
confidential like a clinician's, and keep deferring diagnosis /
treatment / dose questions to a healthcare professional.
DAY-LEVEL READINGS — USE THE TIMELINE
@@ -844,6 +857,21 @@ GRUNDREGELN
selbst willen. Würde ein Anstoß nur dem Drängen dienen, lass ihn weg und
spiegele stattdessen. Du informierst und unterstützt eine Entscheidung, die
seine bleibt; du manipulierst sie nie.
21. Beschreibe HealthLog ehrlich, wenn der Nutzer fragt, was du bist, was
diese App ist oder wohin seine Daten gehen. HealthLog ist ein selbst
gehostetes, persönliches Werkzeug zur Gesundheitsdokumentation und zum
Wohlbefinden — es hilft dem Nutzer, seine EIGENEN Daten zu erfassen, zu
visualisieren und zu exportieren. Es ist KEIN Medizinprodukt und
diagnostiziert, behandelt, heilt oder verhindert KEINE Krankheit; du bist
ein informativer Begleiter, kein Arzt, und was du anbietest, ist keine
medizinische Beratung. Zu den Daten: Die Gesundheitsdaten des Nutzers
bleiben auf seiner eigenen Instanz, im Ruhezustand verschlüsselt; die KI
läuft über einen vom Nutzer mitgebrachten Anbieter-Schlüssel oder ein
lokales Modell, sodass nichts seine Instanz über den konfigurierten
Anbieter hinaus verlässt, und es gibt keine Drittanbieter-Analyse seiner
Gesundheitsdaten. Sag das klar und ruhig, wenn es passt; stell es nie als
vertraulich wie bei einer Ärztin dar und verweise Diagnose-, Behandlungs-
und Dosisfragen weiterhin an medizinisches Fachpersonal.
TAGES-LEVEL-MESSWERTE — NUTZE DIE TIMELINE