diff --git a/src/app/settings/[section]/page.tsx b/src/app/settings/[section]/page.tsx
index 1210e2074..38e5fadae 100644
--- a/src/app/settings/[section]/page.tsx
+++ b/src/app/settings/[section]/page.tsx
@@ -11,7 +11,6 @@ import { ExportSection } from "@/components/settings/export-section";
import { IntegrationsSection } from "@/components/settings/integrations-section";
import { NotificationsSection } from "@/components/settings/notifications-section";
import { SectionPlaceholder } from "@/components/settings/section-placeholder";
-import { SharingSection } from "@/components/settings/sharing-section";
// v1.8.7.1 — `thresholds` (Targets) and `sources` (Sources) are two
// separate sections again. `ThresholdsSection` renders the target-range
// editor; `SourcesSection` (standalone mode) renders the source-priority
@@ -53,7 +52,6 @@ const SECTION_COMPONENTS: Record<
thresholds: ThresholdsSection,
sources: () => ,
api: ApiSection,
- sharing: SharingSection,
export: ExportSection,
advanced: AdvancedSection,
};
diff --git a/src/components/settings/__tests__/arztbericht-hero-card.test.tsx b/src/components/settings/__tests__/arztbericht-hero-card.test.tsx
deleted file mode 100644
index 06c1076cc..000000000
--- a/src/components/settings/__tests__/arztbericht-hero-card.test.tsx
+++ /dev/null
@@ -1,121 +0,0 @@
-/**
- * Arztbericht (doctor-report) card contract.
- *
- * v1.4.37 W7a promoted this surface to the page hero; v1.12 demotes it
- * back to a small secondary card at the bottom of `/settings/export`
- * (the health-record export now owns the hero). The card stays fully
- * functional — only the framing and visual weight changed. This suite
- * pins the SSR contract:
- *
- * 1. The card renders with the expected `data-testid` slot.
- * 2. The value statement is present (Marc-Voice copy, no AI mention).
- * 3. The CTA button is present, clears the 44 px touch floor, and
- * carries an `aria-describedby` link to the value statement.
- * 4. The CTA is wired to a click handler (the prop `data-testid`
- * surface is the e2e contract).
- * 5. Both DE and EN locales resolve their copy without leaking the
- * raw i18n key.
- *
- * Project convention is SSR-only tests (no `@testing-library/react`).
- * The actual click-flow + PDF generation is exercised by the e2e
- * Playwright path and the dialog's own integration tests.
- */
-
-import { describe, expect, it, vi } from "vitest";
-import { renderToStaticMarkup } from "react-dom/server";
-
-vi.mock("next/navigation", () => ({
- useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
- usePathname: () => "/settings/export",
-}));
-
-import { I18nProvider } from "@/lib/i18n/context";
-import { ArztberichtHeroCard } from "../arztbericht-hero-card";
-
-function renderSSR(node: React.ReactElement, locale: "en" | "de" = "en") {
- return renderToStaticMarkup(
- {node},
- );
-}
-
-describe(" — SSR contract", () => {
- it("renders the card slot with eyebrow + title", () => {
- const html = renderSSR();
- expect(html).toContain('data-testid="export-hero-doctor-report"');
- // Title falls back to the existing doctorReport.title key — keeps
- // a single source-of-truth for the brand name.
- expect(html).toContain("Doctor Report");
- // Eyebrow surfaces the use-case framing.
- expect(html).toContain("Doctor visit");
- });
-
- it("renders the value statement as a Marc-Voice one-liner", () => {
- const html = renderSSR();
- expect(html).toContain('data-testid="export-hero-doctor-report-value"');
- expect(html).toContain("printable PDF report");
- // No AI/Coach name-drop in the user-facing artefact (Marc-Voice).
- expect(html.toLowerCase()).not.toContain("coach");
- expect(html.toLowerCase()).not.toContain(" ai ");
- });
-
- it("renders the CTA with min-h-11 touch target + aria-describedby", () => {
- const html = renderSSR();
- expect(html).toContain('data-testid="export-hero-doctor-report-action"');
- // 44 px touch floor (WCAG 2.5.5) — the demoted CTA keeps `min-h-11`
- // on mobile and falls back to the compact `sm:min-h-9` on pointer
- // devices.
- expect(html).toContain("min-h-11");
- // Aria-describedby links the CTA to the value statement so screen
- // readers announce the framing alongside the action label.
- expect(html).toMatch(/aria-describedby="[^"]+"/);
- });
-
- it("does NOT leak raw i18n keys", () => {
- const html = renderSSR();
- expect(html).not.toContain("settings.sections.export.hero");
- expect(html).not.toContain("settings.sections.export.cards.doctorReport");
- });
-
- it("renders German copy when the DE locale is active", () => {
- const html = renderSSR(, "de");
- expect(html).toContain("Arztbericht");
- expect(html).toContain("Arzttermin");
- // Umlauts encoded as their literal UTF-8 character (not their HTML
- // entity), matching the rest of the codebase.
- expect(html).toContain("druckbarer PDF-Bericht");
- expect(html).not.toContain("settings.sections.export.hero");
- });
-
- it("renders as a plain secondary card, not a hero", () => {
- const html = renderSSR();
- // v1.12 demoted the doctor-report from the page hero — the
- // health-record export now owns the hero treatment, so this card
- // must NOT carry the hero gradient/glow utilities.
- expect(html).not.toContain("hero-gradient");
- expect(html).not.toContain("glow-purple");
- // It renders with the standard secondary-card surface.
- expect(html).toContain("bg-card");
- });
-
- it("does not paint dialog markup in the closed-state SSR pass", () => {
- // Radix Dialog renders its portal content lazily, so the closed
- // SSR pass never carries `role="dialog"`. The CTA testid IS
- // present so the e2e suite can drive the open from there. Pinning
- // this contract guards against an accidental `defaultOpen={true}`
- // regression that would briefly paint the dialog above the page on
- // every cold mount.
- const html = renderSSR();
- expect(html).toContain('data-testid="export-hero-doctor-report-action"');
- expect(html).not.toContain('role="dialog"');
- });
-});
-
-describe(" — heading hierarchy", () => {
- it("uses an
so it nests cleanly below the page-level
", () => {
- const html = renderSSR();
- // Page-level h1 (settings-section-export-title) wraps the section;
- // hero owns the h2 slot. The export-section test pins the outer
- // h1; this assertion pins the hero's contribution.
- expect(html).toMatch(/
]*id="export-hero-doctor-report-title"/);
- });
-});
diff --git a/src/components/settings/__tests__/export-section.test.tsx b/src/components/settings/__tests__/export-section.test.tsx
index 1cfdfd331..7f6496218 100644
--- a/src/components/settings/__tests__/export-section.test.tsx
+++ b/src/components/settings/__tests__/export-section.test.tsx
@@ -7,18 +7,16 @@
* - Medications CSV (with optional intake-history toggle)
* - Mood CSV
* - Full JSON Backup
- * - Doctor Report (PDF) — small secondary card at the bottom (v1.12)
*
* Each card must surface a title, a 1-line description, and a
* download/generate button. SSR-only smoke test — interaction is
* exercised by the e2e suite.
*
- * v1.12 — the health-record export takes the page hero; the
- * doctor-report card (``) is demoted to a small
- * secondary card at the bottom of the page. The four CSV/JSON tiles
- * stay under a "Weitere Export-Optionen" / "Other export options"
- * sub-heading. Each component owns its own contract test; this suite
- * pins the page-level shape.
+ * The health-record export takes the page hero. The doctor-report PDF
+ * now lives under the health-record export and is no longer offered as
+ * a separate card here. The four CSV/JSON tiles stay under a "Weitere
+ * Export-Optionen" / "Other export options" sub-heading. Each component
+ * owns its own contract test; this suite pins the page-level shape.
*/
import { describe, expect, it, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
@@ -68,12 +66,11 @@ describe(" — SSR smoke", () => {
expect(html).toContain("glow-purple");
});
- it("mounts the demoted doctor-report card", () => {
+ it("no longer mounts a separate doctor-report card", () => {
const html = render();
- // The card owns its own contract suite; here we pin that the page
- // mounts it, and that the legacy in-grid doctor-report card is
- // gone. The card no longer carries the hero gradient/glow (v1.12).
- expect(html).toContain('data-testid="export-hero-doctor-report"');
+ // The doctor-report PDF now lives under the health-record export;
+ // the Export page no longer offers it as a separate surface.
+ expect(html).not.toContain('data-testid="export-hero-doctor-report"');
expect(html).not.toContain('data-testid="export-card-doctor-report"');
});
@@ -107,19 +104,16 @@ describe(" — SSR smoke", () => {
expect(html).toContain('data-testid="export-medications-include-intake"');
});
- it("renders a download/generate button per secondary card + the hero CTA", () => {
+ it("renders a download/generate button per secondary card", () => {
const html = render();
- // Four secondary cards each carry an `export-action-*` testid; the
- // hero owns the `export-hero-doctor-report-action` slot.
+ // Four secondary cards each carry an `export-action-*` testid.
const buttonMatches = html.match(/data-testid="export-action-/g);
expect(buttonMatches?.length ?? 0).toBe(4);
- expect(html).toContain('data-testid="export-hero-doctor-report-action"');
});
it("German locale renders the DE heading + hero copy", () => {
const html = render(, "de");
expect(html).toContain("Export");
- expect(html).toContain("Arztbericht");
expect(html).toContain("Weitere Export-Optionen");
expect(html).not.toContain("settings.sections.export.");
});
diff --git a/src/components/settings/__tests__/settings-shell.test.tsx b/src/components/settings/__tests__/settings-shell.test.tsx
index 5584e3330..25e3fcb6b 100644
--- a/src/components/settings/__tests__/settings-shell.test.tsx
+++ b/src/components/settings/__tests__/settings-shell.test.tsx
@@ -48,8 +48,6 @@ describe("SETTINGS_SECTION_SLUGS", () => {
// IW-D merged it into `thresholds`.
// v1.8.7.1 — `sources` (Sources) is its own slug again, sitting
// between `thresholds` (Targets) and `ai`.
- // v1.11.0 — `sharing` (clinician share links) added between `api` and
- // `export`. Section count: 12.
expect([...SETTINGS_SECTION_SLUGS]).toEqual([
"account",
"integrations",
@@ -59,7 +57,6 @@ describe("SETTINGS_SECTION_SLUGS", () => {
"sources",
"ai",
"api",
- "sharing",
"export",
"advanced",
"about",
diff --git a/src/components/settings/__tests__/sharing-section.test.tsx b/src/components/settings/__tests__/sharing-section.test.tsx
deleted file mode 100644
index f09fa06e4..000000000
--- a/src/components/settings/__tests__/sharing-section.test.tsx
+++ /dev/null
@@ -1,125 +0,0 @@
-/**
- * v1.11.0 — Settings → Sharing (Epic C, C7).
- *
- * The owner share-link surface. The security-load-bearing guarantees the
- * test pins:
- * 1. The list never carries a raw `hls_` token — the server stores only the
- * hash, so the list response shape has no token field and the rendered
- * markup must not surface one.
- * 2. The one-time token reveal card (`share-token-reveal`) is absent until a
- * create succeeds — on first paint (no token in state) it must not render.
- * 3. Active links render with a revoke control; revoked/expired links fold
- * into the inactive list.
- * 4. The expiry input is bounded at the server cap (90 days).
- *
- * SSR-static render only (no jsdom), matching the sibling settings-section
- * tests. The TanStack hooks are mocked so the query returns a fixed link list.
- */
-import { describe, expect, it, vi } from "vitest";
-import { renderToStaticMarkup } from "react-dom/server";
-
-vi.mock("@/hooks/use-auth", () => ({
- useAuth: () => ({
- user: { id: "u1", role: "USER" },
- isAuthenticated: true,
- isLoading: false,
- refetch: vi.fn(),
- }),
-}));
-
-const FIXTURE_LINKS = [
- {
- id: "link-active",
- label: "Cardiology referral",
- rangeStart: "2026-01-01T00:00:00.000Z",
- rangeEnd: null,
- resourceTypes: ["Patient", "Observation"],
- allowFhirApi: true,
- expiresAt: "2099-01-01T00:00:00.000Z",
- createdAt: "2026-06-01T00:00:00.000Z",
- revokedAt: null,
- lastAccessAt: "2026-06-02T10:00:00.000Z",
- accessCount: 3,
- active: true,
- },
- {
- id: "link-revoked",
- label: "Old GP link",
- rangeStart: "2026-01-01T00:00:00.000Z",
- rangeEnd: null,
- resourceTypes: [],
- allowFhirApi: false,
- expiresAt: "2099-01-01T00:00:00.000Z",
- createdAt: "2026-05-01T00:00:00.000Z",
- revokedAt: "2026-05-15T00:00:00.000Z",
- lastAccessAt: null,
- accessCount: 0,
- active: false,
- },
-];
-
-vi.mock("@tanstack/react-query", () => ({
- useQuery: () => ({ data: FIXTURE_LINKS, isLoading: false }),
- useMutation: () => ({ mutate: vi.fn(), isPending: false }),
- useQueryClient: () => ({ invalidateQueries: vi.fn() }),
-}));
-
-import { I18nProvider } from "@/lib/i18n/context";
-import { SharingSection } from "../sharing-section";
-
-function render(locale: "en" | "de" = "en") {
- return renderToStaticMarkup(
-
-
- ,
- );
-}
-
-describe(" — owner share-link surface (C7)", () => {
- it("renders the create form with a 90-day-capped expiry input", () => {
- const html = render();
- expect(html).toContain("New share link");
- // The expiry field carries the server's max-days cap.
- expect(html).toContain('max="90"');
- expect(html).toContain("Create link");
- });
-
- it("does not reveal a token before a create succeeds", () => {
- const html = render();
- // The one-time reveal card only mounts once `newToken` state is set.
- expect(html).not.toContain('data-testid="share-token-reveal"');
- });
-
- it("lists the active link with a revoke control and never a raw token", () => {
- const html = render();
- expect(html).toContain('data-testid="share-active-list"');
- expect(html).toContain("Cardiology referral");
- expect(html).toContain("Revoke");
- // The list response shape carries no token; the markup must not surface
- // an `hls_` string anywhere.
- expect(html).not.toContain("hls_");
- // The FHIR badge surfaces for the FHIR-enabled link.
- expect(html).toContain("FHIR");
- });
-
- it("folds revoked links into the inactive list (not the active list)", () => {
- const html = render();
- // The active list must NOT contain the revoked link's label.
- const activeListStart = html.indexOf('data-testid="share-active-list"');
- const activeListEnd = html.indexOf('data-testid="share-inactive-list"');
- // Inactive list is collapsed by default, so the label only appears once
- // the disclosure is open — but the active list must never carry it.
- const activeSlice =
- activeListEnd > activeListStart
- ? html.slice(activeListStart, activeListEnd)
- : html.slice(activeListStart);
- expect(activeSlice).not.toContain("Old GP link");
- // The inactive-count disclosure surfaces.
- expect(html).toContain("Revoked and expired");
- });
-
- it("renders localized section copy", () => {
- const html = render("de");
- expect(html).toContain("Neuer Freigabe-Link");
- });
-});
diff --git a/src/components/settings/arztbericht-hero-card.tsx b/src/components/settings/arztbericht-hero-card.tsx
deleted file mode 100644
index 7783457da..000000000
--- a/src/components/settings/arztbericht-hero-card.tsx
+++ /dev/null
@@ -1,178 +0,0 @@
-"use client";
-
-/**
- * Settings → Export Arztbericht (doctor-report) card.
- *
- * The doctor-report PDF is the artefact a user carries into a doctor's
- * appointment. v1.4.37 W7a promoted it to the page hero; v1.12 demotes
- * it back to a small secondary card at the bottom of the export page —
- * the health-record export now owns the page hero. The card is fully
- * functional: the CTA routes through the existing ``
- * + `/api/doctor-report` flow exactly as before. Only the framing and
- * visual weight changed.
- *
- * Layout (compact card, no hero gradient):
- *
- * ┌─ card ────────────────────────────────────────────┐
- * │ [icon] H2 title eyebrow │
- * │ one-line value statement │
- * │ [Configure & generate] PDF · printable │
- * └───────────────────────────────────────────────────┘
- *
- * a11y:
- * - The CTA button clears the 44 px touch floor (`min-h-11`).
- * - `focus-visible:ring` ships via the default `
)}
-
+
= {
sources: Layers,
ai: Sparkles,
api: KeyRound,
- sharing: Share2,
export: Download,
advanced: Settings2,
about: Info,
diff --git a/src/components/settings/section-slugs.ts b/src/components/settings/section-slugs.ts
index 2b83ff968..76f010ef5 100644
--- a/src/components/settings/section-slugs.ts
+++ b/src/components/settings/section-slugs.ts
@@ -22,10 +22,6 @@ export const SETTINGS_SECTION_SLUGS = [
"sources",
"ai",
"api",
- // v1.11.0 — `sharing` owns clinician share links: a time-boxed, scope-
- // frozen read-only view of the owner's record, optionally a scoped FHIR
- // face. Sits next to `api` (the other "hand a credential out" surface).
- "sharing",
"export",
"advanced",
"about",
diff --git a/src/components/settings/settings-shell.tsx b/src/components/settings/settings-shell.tsx
index 954475e1f..22043e34a 100644
--- a/src/components/settings/settings-shell.tsx
+++ b/src/components/settings/settings-shell.tsx
@@ -25,7 +25,6 @@ import {
LayoutDashboard,
Link2,
Settings2,
- Share2,
SlidersHorizontal,
Sparkles,
User,
@@ -110,11 +109,6 @@ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [
},
{ slug: "ai", titleKey: "settings.sections.ai.title", icon: Sparkles },
{ slug: "api", titleKey: "settings.sections.api.title", icon: KeyRound },
- {
- slug: "sharing",
- titleKey: "settings.sections.sharing.title",
- icon: Share2,
- },
{
slug: "export",
titleKey: "settings.sections.export.title",
diff --git a/src/components/settings/sharing-section.tsx b/src/components/settings/sharing-section.tsx
deleted file mode 100644
index 55096772d..000000000
--- a/src/components/settings/sharing-section.tsx
+++ /dev/null
@@ -1,505 +0,0 @@
-"use client";
-
-/**
- * v1.11.0 — Settings → Sharing (Epic C, C7).
- *
- * The OWNER surface for clinician share links. A share link is a time-boxed,
- * scope-frozen, read-only view of the owner's own health record at
- * `/c/`, optionally exposing a scoped read-only FHIR face. The raw
- * `hls_` token is shown EXACTLY ONCE on create — the list never carries it
- * (the server only stores its hash), so the copy-on-create card is the single
- * chance to capture it.
- *
- * Client island: the create form, the active/revoked lists, and the revoke
- * action all need state and mutate via the C4 lifecycle API
- * (`/api/share-links`). Reads unwrap `(await res.json()).data`; the query key
- * comes from the centralised factory. No markdown anywhere — every value
- * renders as escaped React text.
- */
-import { useMemo, useState } from "react";
-import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
-import { Copy, Loader2, Share2, Trash2 } from "lucide-react";
-
-import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
- AlertDialogTrigger,
-} from "@/components/ui/alert-dialog";
-import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
-import { Checkbox } from "@/components/ui/checkbox";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import { Switch } from "@/components/ui/switch";
-import { SettingsCardHeader } from "@/components/settings/_card-header";
-import { useAuth } from "@/hooks/use-auth";
-import { formatDate, formatDateTime } from "@/lib/format";
-import { useTranslations } from "@/lib/i18n/context";
-import { queryKeys } from "@/lib/query-keys";
-
-/** The FHIR resource types a share link may serve — mirrors C4's enum. */
-const RESOURCE_TYPES = [
- "Patient",
- "Observation",
- "MedicationStatement",
- "MedicationAdministration",
-] as const;
-type ResourceType = (typeof RESOURCE_TYPES)[number];
-
-/** Maximum lifetime, in days — mirrors `SHARE_LINK_MAX_DAYS` on the server. */
-const MAX_DAYS = 90;
-const DEFAULT_DAYS = 30;
-
-/** Owner-facing shape returned by `GET /api/share-links` (never the token). */
-interface ShareLinkSummary {
- id: string;
- label: string;
- rangeStart: string;
- rangeEnd: string | null;
- resourceTypes: string[];
- allowFhirApi: boolean;
- expiresAt: string;
- createdAt: string;
- revokedAt: string | null;
- lastAccessAt: string | null;
- accessCount: number;
- active: boolean;
-}
-
-export function SharingSection() {
- const { t } = useTranslations();
-
- return (
-
-
-