From 79ce836335373272534eb94741e8df718129ded7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 6 Jun 2026 23:14:04 +0200 Subject: [PATCH 01/11] fix(settings): guard notification panel against non-array status payload The notifications settings panel white-screened with "x.map is not a function" when a status payload reached the card as the whole { channels, events } object instead of the channels array (events is an object map, not an array). Guard the channel map with an Array.isArray check, default the mood reminder-hour read to optional chaining, and add a regression test that renders the empty state instead of crashing on a malformed payload. --- .../medications/sections/notifications-section.tsx | 2 +- .../__tests__/notification-status-card.test.tsx | 14 ++++++++++++++ src/components/settings/mood-reminder-card.tsx | 2 +- .../settings/notification-status-card.tsx | 7 ++++++- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/components/medications/sections/notifications-section.tsx b/src/components/medications/sections/notifications-section.tsx index d4a089453..07d2eccff 100644 --- a/src/components/medications/sections/notifications-section.tsx +++ b/src/components/medications/sections/notifications-section.tsx @@ -201,7 +201,7 @@ export function NotificationsBody({ )} - {status && status.channels.length > 0 && ( + {Array.isArray(status?.channels) && status.channels.length > 0 && ( - - ); -} From eda79794d9c417c32aec26cfffc9418b8c8ebe2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 6 Jun 2026 23:14:50 +0200 Subject: [PATCH 03/11] feat(settings): trim the export surface and remove the sharing page Right-align the health-record export generate button. Remove the duplicate doctor-report card from the export page (the doctor report is reached through the health-record export now) and drop the now-unused ArztberichtHeroCard. Remove the clinician share-link ("sharing") page, its nav entry, slug, and section mapping; the backing model, routes, and public clinician view stay in place for a later dedicated removal. --- src/app/settings/[section]/page.tsx | 2 - .../__tests__/arztbericht-hero-card.test.tsx | 121 ----- .../__tests__/export-section.test.tsx | 28 +- .../__tests__/settings-shell.test.tsx | 3 - .../__tests__/sharing-section.test.tsx | 125 ----- .../settings/arztbericht-hero-card.tsx | 178 ------ src/components/settings/export-section.tsx | 14 +- .../settings/health-record-export-panel.tsx | 2 +- .../settings/section-placeholder.tsx | 2 - src/components/settings/section-slugs.ts | 4 - src/components/settings/settings-shell.tsx | 6 - src/components/settings/sharing-section.tsx | 505 ------------------ 12 files changed, 15 insertions(+), 975 deletions(-) delete mode 100644 src/components/settings/__tests__/arztbericht-hero-card.test.tsx delete mode 100644 src/components/settings/__tests__/sharing-section.test.tsx delete mode 100644 src/components/settings/arztbericht-hero-card.tsx delete mode 100644 src/components/settings/sharing-section.tsx 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 ` - - {t("settings.sections.export.hero.formatHint")} - - - - {error && ( -

- {error} -

- )} - - - - - ); -} diff --git a/src/components/settings/export-section.tsx b/src/components/settings/export-section.tsx index 4330603aa..c4d0be2a4 100644 --- a/src/components/settings/export-section.tsx +++ b/src/components/settings/export-section.tsx @@ -16,13 +16,11 @@ * 3. Medications CSV — optional intake-history toggle * 4. Mood CSV — optional `since`/`until` * 5. Full JSON Backup — single-file user-scoped dump - * 6. Doctor Report (PDF) — small secondary card at the bottom - * (``) * * v1.12 — the health-record export is the headline "data out" path; it - * takes the page hero. The doctor-report PDF is demoted to a small - * secondary card at the bottom of the page — fully functional, just no - * longer the flagship surface it was in v1.4.37 W7a. + * takes the page hero. The doctor-report PDF lives under the + * health-record export now and is no longer offered as a separate card + * here (it used to be a small secondary card at the bottom). * * Mobile-first: cards stack on `=md`. */ @@ -42,7 +40,6 @@ import { Button } from "@/components/ui/button"; import { DateInput } from "@/components/ui/date-input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; -import { ArztberichtHeroCard } from "@/components/settings/arztbericht-hero-card"; import { HealthRecordExportPanel } from "@/components/settings/health-record-export-panel"; import { useTranslations } from "@/lib/i18n/context"; @@ -114,11 +111,6 @@ export function ExportSection() { - - {/* v1.12 — doctor-report PDF, demoted from the former page hero to a - small secondary card at the bottom. Fully functional — the same - `` + `/api/doctor-report` flow runs from here. */} - ); } diff --git a/src/components/settings/health-record-export-panel.tsx b/src/components/settings/health-record-export-panel.tsx index cefedf981..54fe6e37c 100644 --- a/src/components/settings/health-record-export-panel.tsx +++ b/src/components/settings/health-record-export-panel.tsx @@ -433,7 +433,7 @@ export function HealthRecordExportPanel() {

)} -
+
- - - {newToken && ( -
-

- {t("settings.sharing.tokenCreated")} -

-

- {t("settings.sharing.tokenOnce")} -

-
- - {`${typeof window !== "undefined" ? window.location.origin : ""}/c/${newToken}`} - - -
- {copied && ( -

- {t("settings.sharing.copied")} -

- )} -
- )} - -
-

- {t("settings.sharing.activeTitle")} -

- {activeLinks.length === 0 ? ( -

- {t("settings.sharing.noActive")} -

- ) : ( -
    - {activeLinks.map((link) => ( -
  • -
    -

    - {link.label} -

    -
    - - {t("settings.sharing.statusActive")} - - {link.allowFhirApi && ( - - FHIR - - )} -
    -
    -

    - - {t("settings.sharing.created")}: - {" "} - {formatDate(link.createdAt)} -

    -

    - - {t("settings.sharing.expires")}: - {" "} - {formatDateTime(link.expiresAt)} -

    -

    - - {t("settings.sharing.accessCount")}: - {" "} - {link.accessCount} - {link.lastAccessAt - ? ` · ${formatDateTime(link.lastAccessAt)}` - : ""} -

    - - - - - - - - {t("settings.sharing.revoke")} - - - {t("settings.sharing.revokeDescription")} - - - - - {t("common.cancel")} - - revokeMutation.mutate(link.id)} - > - {t("settings.sharing.revoke")} - - - - -
  • - ))} -
- )} -
- - {inactiveLinks.length > 0 && ( -
- - {showRevoked && ( -
    - {inactiveLinks.map((link) => ( -
  • -
    -

    - {link.label} -

    - - {link.revokedAt - ? t("settings.sharing.statusRevoked") - : t("settings.sharing.statusExpired")} - -
    -

    - - {t("settings.sharing.accessCount")}: - {" "} - {link.accessCount} -

    -
  • - ))} -
- )} -
- )} -
- ); -} From 1b283a9c01dfbb2ea52854077f6c56e7b16fca46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 6 Jun 2026 23:15:05 +0200 Subject: [PATCH 04/11] feat(settings): land the account page from the avatar, tidy the token row Clicking the avatar (and the name beside it) in the sidebar user menu now routes straight to the account settings. On the per-medication API token row, drop the separate "Copy URL" button in favour of an inline read-only endpoint field with a copy icon, and switch the generate-token button to the standard outline variant so it no longer renders as the lone primary-coloured control on the row. --- src/components/layout/sidebar-nav.tsx | 78 ++++++++++++------- .../medications/sections/api-tokens-row.tsx | 41 ++++++---- 2 files changed, 72 insertions(+), 47 deletions(-) diff --git a/src/components/layout/sidebar-nav.tsx b/src/components/layout/sidebar-nav.tsx index 4e62bf571..ba4cf3670 100644 --- a/src/components/layout/sidebar-nav.tsx +++ b/src/components/layout/sidebar-nav.tsx @@ -124,22 +124,31 @@ function SidebarUserSection({ collapsed }: { collapsed: boolean }) { className="w-60" sideOffset={8} > -
- - {avatarUrl && } - - {getInitials(user.username)} - - -
-

{user.username}

- {user.email && ( -

- {user.email} -

- )} -
-
+ {/* R20 — the user header is the direct route into the account + settings. Clicking the avatar / name jumps straight to + `/settings/account` rather than making the user hunt for a + separate "Settings" entry. */} + + + + {avatarUrl && } + + {getInitials(user.username)} + + +
+

{user.username}

+ {user.email && ( +

+ {user.email} +

+ )} +
+ +
@@ -219,20 +228,29 @@ function SidebarUserSection({ collapsed }: { collapsed: boolean }) { return (
- - {avatarUrl && } - - {getInitials(user.username)} - - -
-

{user.username}

- {user.email && ( -

- {user.email} -

- )} -
+ {/* R20 — the avatar + name in the expanded footer routes straight + into the account settings; the kebab to the right still opens + the rest of the user menu. */} + + + {avatarUrl && } + + {getInitials(user.username)} + + +
+

{user.username}

+ {user.email && ( +

+ {user.email} +

+ )} +
+ {t("medications.detail.api.caption", { name: medicationName })}

- {/* The raw endpoint URL is redundant with the copy button below, so - it is rendered muted + single-line-truncated rather than as a - full-width block — the long absolute URL was pure visual noise. */} - ` carries the value; the icon button copies it. */} +
- POST {endpoint} - + + POST {endpoint} + + +
-