diff --git a/messages/de.json b/messages/de.json index 144d7869b..61ab350c6 100644 --- a/messages/de.json +++ b/messages/de.json @@ -2783,6 +2783,7 @@ "settingsVerbosityBrief": "Kurz", "settingsVerbosityDefault": "Standard", "settingsVerbosityDetailed": "Ausführlich", + "settingsVerbosityConciseHint": "Der knappe Tonfall hält Antworten ohnehin kurz — die Ausführlichkeit ist dabei fest auf „Kurz“ gesetzt.", "settingsSourcesPointer": "Welche Datenquellen der Coach nutzen darf und das Standard-Analysefenster verwaltest du direkt im Coach-Chat (Leiste „Worauf ich zugreife\").", "settingsSourcesPointerLink": "Coach öffnen", "settingsExcludeLabel": "Metriken ausschließen", @@ -5952,6 +5953,7 @@ "repository": "Quellcode", "changelog": "Changelog", "docs": "Dokumentation", + "credits": "Credits & Datenquellen", "linksHeading": "Quellen & Dokumentation", "newerAvailable": "Neue Version: {tag}" }, diff --git a/messages/en.json b/messages/en.json index 757706fb8..53dcb2ace 100644 --- a/messages/en.json +++ b/messages/en.json @@ -2783,6 +2783,7 @@ "settingsVerbosityBrief": "Brief", "settingsVerbosityDefault": "Default", "settingsVerbosityDetailed": "Detailed", + "settingsVerbosityConciseHint": "The concise tone already keeps replies short — verbosity is pinned to Brief while it is active.", "settingsSourcesPointer": "Which data clusters the coach may draw on and the default analysis window are managed directly in the coach chat (\"What I draw on\" rail).", "settingsSourcesPointerLink": "Open the coach", "settingsExcludeLabel": "Exclude metrics", @@ -5952,6 +5953,7 @@ "repository": "Source code", "changelog": "Changelog", "docs": "Documentation", + "credits": "Credits & data attributions", "linksHeading": "Sources & docs", "newerAvailable": "Newer version: {tag}" }, diff --git a/messages/es.json b/messages/es.json index 36bbb67b4..1ec276653 100644 --- a/messages/es.json +++ b/messages/es.json @@ -2783,6 +2783,7 @@ "settingsVerbosityBrief": "Breve", "settingsVerbosityDefault": "Estándar", "settingsVerbosityDetailed": "Detallado", + "settingsVerbosityConciseHint": "El tono conciso ya mantiene las respuestas cortas: la extensión queda fijada en «Breve» mientras esté activo.", "settingsSourcesPointer": "Los grupos de datos que el coach puede usar y la ventana de análisis predeterminada se gestionan directamente en el chat del coach (panel «En qué me baso»).", "settingsSourcesPointerLink": "Abrir el coach", "settingsExcludeLabel": "Excluir métricas", @@ -5952,6 +5953,7 @@ "repository": "Código fuente", "changelog": "Changelog", "docs": "Documentación", + "credits": "Créditos y atribuciones de datos", "linksHeading": "Fuentes y documentación", "newerAvailable": "Nueva versión: {tag}" }, diff --git a/messages/fr.json b/messages/fr.json index be0c38ba8..c98f3feff 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -2783,6 +2783,7 @@ "settingsVerbosityBrief": "Bref", "settingsVerbosityDefault": "Standard", "settingsVerbosityDetailed": "Détaillé", + "settingsVerbosityConciseHint": "Le ton concis garde déjà les réponses courtes — la longueur reste fixée sur « Bref » tant qu'il est actif.", "settingsSourcesPointer": "Les groupes de données que le coach peut utiliser et la fenêtre d'analyse par défaut se gèrent directement dans le chat du coach (volet « Ce sur quoi je m'appuie »).", "settingsSourcesPointerLink": "Ouvrir le coach", "settingsExcludeLabel": "Exclure des métriques", @@ -5952,6 +5953,7 @@ "repository": "Code source", "changelog": "Changelog", "docs": "Documentation", + "credits": "Crédits et attributions de données", "linksHeading": "Sources et documentation", "newerAvailable": "Nouvelle version : {tag}" }, diff --git a/messages/it.json b/messages/it.json index 71d04c96c..85ee1f7be 100644 --- a/messages/it.json +++ b/messages/it.json @@ -2783,6 +2783,7 @@ "settingsVerbosityBrief": "Breve", "settingsVerbosityDefault": "Standard", "settingsVerbosityDetailed": "Dettagliata", + "settingsVerbosityConciseHint": "Il tono conciso mantiene già le risposte brevi: la verbosità resta fissata su «Breve» finché è attivo.", "settingsSourcesPointer": "I gruppi di dati che il coach può usare e la finestra di analisi predefinita si gestiscono direttamente nella chat del coach (pannello «Su cosa mi baso»).", "settingsSourcesPointerLink": "Apri il coach", "settingsExcludeLabel": "Escludi metriche", @@ -5952,6 +5953,7 @@ "repository": "Codice sorgente", "changelog": "Changelog", "docs": "Documentazione", + "credits": "Crediti e attribuzioni dei dati", "linksHeading": "Fonti e documentazione", "newerAvailable": "Nuova versione: {tag}" }, diff --git a/messages/pl.json b/messages/pl.json index bb3fbb60f..58d4c731c 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -2783,6 +2783,7 @@ "settingsVerbosityBrief": "Krótka", "settingsVerbosityDefault": "Standardowa", "settingsVerbosityDetailed": "Szczegółowa", + "settingsVerbosityConciseHint": "Zwięzły ton i tak skraca odpowiedzi — szczegółowość pozostaje ustawiona na „Krótko”, dopóki jest aktywny.", "settingsSourcesPointer": "Tym, z jakich grup danych może korzystać coach, oraz domyślnym oknem analizy zarządzasz bezpośrednio w czacie coacha (panel „Z czego korzystam”).", "settingsSourcesPointerLink": "Otwórz coacha", "settingsExcludeLabel": "Wyklucz metryki", @@ -5952,6 +5953,7 @@ "repository": "Kod źródłowy", "changelog": "Changelog", "docs": "Dokumentacja", + "credits": "Autorzy i źródła danych", "linksHeading": "Źródła i dokumentacja", "newerAvailable": "Nowa wersja: {tag}" }, diff --git a/src/app/privacy/page.tsx b/src/app/privacy/page.tsx index 6be6417ef..915eb8666 100644 --- a/src/app/privacy/page.tsx +++ b/src/app/privacy/page.tsx @@ -2057,6 +2057,18 @@ export default function PrivacyPage() { . Policy version {POLICY_VERSION}. Last updated {LAST_UPDATED}.

+ {/* The GeoLite2 EULA wants the attribution page reachable from the + running app — this and the Settings→About row are the two + inbound links to /about. */} +

+ + About HealthLog — credits & data attributions + +

diff --git a/src/app/settings/[section]/page.tsx b/src/app/settings/[section]/page.tsx index ee1f341af..572123d39 100644 --- a/src/app/settings/[section]/page.tsx +++ b/src/app/settings/[section]/page.tsx @@ -10,27 +10,16 @@ import { AdvancedSection } from "@/components/settings/advanced-section"; import { AiSection } from "@/components/settings/ai-section"; import { ApiSection } from "@/components/settings/api-section"; import { CoachSection } from "@/components/settings/coach-section"; -import { DashboardSection } from "@/components/settings/dashboard-section"; import { EnvironmentSection } from "@/components/settings/environment-section"; import { ExportSection } from "@/components/settings/export-section"; import { GesundheitsakteSection } from "@/components/settings/gesundheitsakte-section"; -import { IllnessSection } from "@/components/settings/illness-section"; -import { InsightsSection } from "@/components/settings/insights-section"; import { IntegrationsSection } from "@/components/settings/integrations-section"; -import { LabsSection } from "@/components/settings/labs-section"; import { LayoutSection } from "@/components/settings/layout-section"; -import { MedicationsSection } from "@/components/settings/medications-section"; import { McpSection } from "@/components/settings/mcp-section"; import { ModulesSection } from "@/components/settings/modules-section"; -import { MoodSection } from "@/components/settings/mood-section"; import { NotificationsSection } from "@/components/settings/notifications-section"; import { PrivacySection } from "@/components/settings/privacy-section"; import { ScoreSection } from "@/components/settings/score-section"; -import { SectionPlaceholder } from "@/components/settings/section-placeholder"; -import { VorsorgeSection } from "@/components/settings/vorsorge-section"; -// v1.18.1 (D4) — `sources` is a standalone left-side entry (split out of the -// Integrations sub-tabs). v1.25.3 — `channels` folded into Notifications. -import { SharingSection } from "@/components/settings/sharing-section"; import { SourcesSection } from "@/components/settings/sources-section"; import { ThresholdsSection } from "@/components/settings/thresholds-section"; import { @@ -72,21 +61,13 @@ const SECTION_COMPONENTS: Record< sources: SourcesSection, notifications: NotificationsSection, layout: LayoutSection, - dashboard: DashboardSection, - insights: InsightsSection, - medications: MedicationsSection, - mood: MoodSection, - labs: LabsSection, - illness: IllnessSection, environment: EnvironmentSection, anamnesis: AnamnesisSection, score: ScoreSection, - vorsorge: VorsorgeSection, thresholds: ThresholdsSection, api: ApiSection, mcp: McpSection, gesundheitsakte: GesundheitsakteSection, - sharing: SharingSection, export: ExportSection, advanced: AdvancedSection, privacy: PrivacySection, @@ -107,32 +88,25 @@ export default async function SettingsSectionPage({ params }: PageProps) { notFound(); } + // `SECTION_COMPONENTS` is an exhaustive Record over the slug union, so a + // slug without a wired component is a compile error — the old runtime + // `` fallback could never render and was removed. const SectionComponent = SECTION_COMPONENTS[section]; - // Defensive fallback: in theory unreachable since `isSettingsSectionSlug` - // guards the slug, but a future slug added to `SETTINGS_SECTION_SLUGS` - // without a wired component would otherwise crash silently — placeholder - // surfaces the gap visually instead. // v1.18.6.1 — the heading + subtitle (and the Layout-hub "← back" link) - // now live in ``, which places them in their own grid row + // live in ``, which places them in their own grid row // spanning only the content column so the left nav's first item lines up // with the top of the first card. The page body is pure card content, // wrapped in the labelled `
` so the historic // `settings-section--title` `aria-labelledby` linkage still resolves. - const body = SectionComponent ? ( -
- -
- ) : ( - - ); - return ( - {body} +
+ +
); diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index dd87ccfd6..ad45441fa 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -5,10 +5,8 @@ import { permanentRedirect } from "next/navigation"; * * Pre-v1.4 the entire settings UI lived in this single 3000+ LOC file. As part * of the v1.4 settings split (PR series A2), each concern moved to its own - * route under `/settings/[section]/page.tsx`. The historic monolith is - * preserved verbatim in `page.legacy.tsx` for section-by-section extraction in - * the follow-up PRs (A2-account, A2-about, A2-ai, A2-integrations, - * A2-notifications, A2-rest). + * route under `/settings/[section]/page.tsx`; the extraction finished long + * ago and the historic monolith is gone. * * 308 (`permanentRedirect`) keeps the request method intact and is the right * answer for a permanent address change — bookmarks, deep-links, and any diff --git a/src/components/documents/__tests__/use-document-upload-settle.test.ts b/src/components/documents/__tests__/use-document-upload-settle.test.ts new file mode 100644 index 000000000..baf75b8a6 --- /dev/null +++ b/src/components/documents/__tests__/use-document-upload-settle.test.ts @@ -0,0 +1,44 @@ +/** + * The upload manager's settle decision (§3.2 + the fresh-upload flash). + * + * Watched red: with `settleDecision` returning `flashId: null` on the + * non-duplicate branch (the pre-v1.37.19 behaviour — only the duplicate + * path flashed the row), the fresh-upload case below fails. A fresh upload + * used to give no feedback beyond its queue row disappearing. + */ +import { describe, expect, it } from "vitest"; + +import { settleDecision } from "../use-document-upload"; +import type { UploadResult } from "../vault-utils"; + +function success(duplicate: boolean): UploadResult { + return { + ok: true, + duplicate, + document: { id: "doc-1" }, + } as unknown as UploadResult; +} + +describe("settleDecision", () => { + it("flashes the NEW row on a fresh upload (no toast)", () => { + expect(settleDecision(success(false))).toEqual({ + removeFromQueue: true, + toastDuplicate: false, + flashId: "doc-1", + }); + }); + + it("flashes the EXISTING row and toasts on a duplicate", () => { + expect(settleDecision(success(true))).toEqual({ + removeFromQueue: true, + toastDuplicate: true, + flashId: "doc-1", + }); + }); + + it("keeps a failed upload in the queue with no flash", () => { + expect( + settleDecision({ ok: false, reason: "generic" } as UploadResult), + ).toEqual({ removeFromQueue: false, toastDuplicate: false, flashId: null }); + }); +}); diff --git a/src/components/documents/use-document-upload.ts b/src/components/documents/use-document-upload.ts index 8f7977a29..ee6f5c1d2 100644 --- a/src/components/documents/use-document-upload.ts +++ b/src/components/documents/use-document-upload.ts @@ -136,6 +136,29 @@ function uploadViaXhr( }); } +/** + * The pure settle decision — what a finished upload does to the UI. + * + * Every success flashes the row (fresh upload → the new row, duplicate → + * the existing row); only the duplicate additionally toasts. Exported for + * the unit test: until v1.37.19 only the duplicate branch flashed, so a + * fresh upload gave no feedback beyond its queue row disappearing. + */ +export function settleDecision(result: UploadResult): { + removeFromQueue: boolean; + toastDuplicate: boolean; + flashId: string | null; +} { + if (!result.ok) { + return { removeFromQueue: false, toastDuplicate: false, flashId: null }; + } + return { + removeFromQueue: true, + toastDuplicate: result.duplicate, + flashId: result.document.id, + }; +} + export interface DocumentUploadManager { /** Live queue — uploading entries first-in-first, then error entries. */ items: UploadQueueItem[]; @@ -196,16 +219,17 @@ export function useDocumentUpload( const settle = useCallback( (localId: string, result: UploadResult) => { - if (result.ok) { - removeItem(localId); - void invalidateKeys(queryClient, [queryKeys.documents()]); - if (result.duplicate) { - toast.info(t("documents.toast.duplicate")); - flashHighlight(result.document.id); - } + if (!result.ok) { + patchItem(localId, { status: "error", failure: result, progress: 0 }); return; } - patchItem(localId, { status: "error", failure: result, progress: 0 }); + const decision = settleDecision(result); + removeItem(localId); + void invalidateKeys(queryClient, [queryKeys.documents()]); + if (decision.toastDuplicate) { + toast.info(t("documents.toast.duplicate")); + } + if (decision.flashId) flashHighlight(decision.flashId); }, [flashHighlight, patchItem, queryClient, removeItem, t], ); diff --git a/src/components/settings/__tests__/integration-status-pill.test.tsx b/src/components/settings/__tests__/integration-status-pill.test.tsx index 8837d489a..b409902db 100644 --- a/src/components/settings/__tests__/integration-status-pill.test.tsx +++ b/src/components/settings/__tests__/integration-status-pill.test.tsx @@ -105,7 +105,7 @@ describe("IntegrationStatusPill", () => { , ); expect(html).toContain("Paused"); - expect(html).toContain("reconnect manually"); + expect(html).toContain("Paused — reconnect"); expect(html).toContain('data-state="parked"'); }); @@ -115,7 +115,7 @@ describe("IntegrationStatusPill", () => { "de", ); expect(html).toContain("Pausiert"); - expect(html).toContain("manuell wieder verbinden"); + expect(html).toContain("Pausiert — neu verbinden"); }); // The three states the server verdict added. `stale` and `stalled` both diff --git a/src/components/settings/__tests__/integrations-section.test.tsx b/src/components/settings/__tests__/integrations-section.test.tsx index b06e412a4..929dfb29e 100644 --- a/src/components/settings/__tests__/integrations-section.test.tsx +++ b/src/components/settings/__tests__/integrations-section.test.tsx @@ -223,7 +223,7 @@ describe("IntegrationsSection — single-status-display contract (A5)", () => { // The pill flips to `data-state="parked"` and the resume banner // renders with the reconnect CTA. The legacy `error` reconnect text // (red) must NOT appear because the user-facing copy is the lighter - // "Paused — reconnect manually" string, not "Error — reconnect". + // "Paused — reconnect" string, not "Error — reconnect". it("Withings parked state surfaces the parked pill + resume CTA", () => { setIntegrationStatus({ threshold: 3, @@ -246,7 +246,7 @@ describe("IntegrationsSection — single-status-display contract (A5)", () => { const html = render(); expect(html).toContain('data-state="parked"'); expect(html).toContain("Paused"); - expect(html).toContain("reconnect manually"); + expect(html).toContain("Paused — reconnect"); expect(html).toContain('data-testid="withings-parked-banner"'); expect(html).toContain('data-testid="withings-resume-button"'); // The lastError is surfaced under the pill so the operator sees diff --git a/src/components/settings/__tests__/record-settings-section-gate.test.tsx b/src/components/settings/__tests__/record-settings-section-gate.test.tsx index 9bac09382..86f73b0ea 100644 --- a/src/components/settings/__tests__/record-settings-section-gate.test.tsx +++ b/src/components/settings/__tests__/record-settings-section-gate.test.tsx @@ -44,7 +44,7 @@ vi.mock("../record-anamnesis-section", () => ({ import { RecordSettingsSectionGate } from "../record-settings-section-gate"; -function render(section: "modules" | "integrations" | "anamnesis" | "labs") { +function render(section: "modules" | "integrations" | "anamnesis" | "score") { return renderToStaticMarkup( @@ -140,7 +140,7 @@ describe("RecordSettingsSectionGate", () => { }); it("does not lend its page to another destination of the same kind", () => { - // `labs` is `adult-shared-unavailable` today, so this passes for the + // `score` is `adult-shared-unavailable` today, so this passes for the // right reason now; the claim it pins is that the branch asks WHICH // page, not which category. A second `manage-writable` slug added // without an arm falls through to the refusal panel. @@ -153,7 +153,7 @@ describe("RecordSettingsSectionGate", () => { canWrite: true, }; - const html = render("labs"); + const html = render("score"); expect(html).not.toContain("record-anamnesis"); expect(html).toContain("shared-record-settings-unavailable-title"); diff --git a/src/components/settings/__tests__/section-frame-harness.tsx b/src/components/settings/__tests__/section-frame-harness.tsx index 33e0fd68e..c01640773 100644 --- a/src/components/settings/__tests__/section-frame-harness.tsx +++ b/src/components/settings/__tests__/section-frame-harness.tsx @@ -15,13 +15,17 @@ import * as React from "react"; import { useTranslations } from "@/lib/i18n/context"; -import type { SettingsSectionSlug } from "../section-slugs"; export function SettingsSectionFrame({ slug, children, }: { - slug: SettingsSectionSlug; + /** + * A section slug OR a layout-hub child id (`dashboard`, `mood`, …) — the + * children left `SETTINGS_SECTION_SLUGS` in v1.37.19 but their heading + * keys and section bodies live on under `/settings/layout/`. + */ + slug: string; children: React.ReactNode; }) { const { t } = useTranslations(); diff --git a/src/components/settings/__tests__/sections.test.tsx b/src/components/settings/__tests__/sections.test.tsx index 6f014ac4d..782bfe7ed 100644 --- a/src/components/settings/__tests__/sections.test.tsx +++ b/src/components/settings/__tests__/sections.test.tsx @@ -115,7 +115,8 @@ function render(node: React.ReactElement, locale: "en" | "de" = "en") { // section in. The heading-level smoke checks render the body THROUGH the frame // (mirroring production) so they assert what the user actually sees. function renderFramed( - slug: SettingsSectionSlug, + // A slug or a layout-hub child id — see the harness note. + slug: SettingsSectionSlug | "dashboard", node: React.ReactElement, locale: "en" | "de" = "en", ) { diff --git a/src/components/settings/__tests__/settings-shell.test.tsx b/src/components/settings/__tests__/settings-shell.test.tsx index 25f80a4c4..8dba31062 100644 --- a/src/components/settings/__tests__/settings-shell.test.tsx +++ b/src/components/settings/__tests__/settings-shell.test.tsx @@ -113,13 +113,12 @@ describe("SETTINGS_SECTION_SLUGS", () => { "integrations", "sources", "notifications", + // v1.37.19 — the per-module personalization slugs (dashboard / + // insights / medications / mood / labs / illness / vorsorge) and + // `sharing` left the list: their URLs are owned by the permanent + // redirects in next.config.ts, so the dynamic route could never + // serve them. "layout", - "dashboard", - "insights", - "medications", - "mood", - "labs", - "illness", // v1.25 (W-ENV) — Environment (home location, travel overrides, backfill), // module-gated on the opt-in `environment` module. "environment", @@ -127,14 +126,12 @@ describe("SETTINGS_SECTION_SLUGS", () => { "anamnesis", // v1.35.0 — the Health Score composition surface. "score", - "vorsorge", "thresholds", "ai", "coach", "api", "mcp", "gesundheitsakte", - "sharing", "export", "advanced", // v1.23 — "Data & Privacy" assembles the export / deletion / retention / @@ -149,6 +146,9 @@ describe("SETTINGS_SECTION_SLUGS", () => { expect(isSettingsSectionSlug(slug)).toBe(true); } expect(isSettingsSectionSlug("nope")).toBe(false); + // The redirect-shadowed module slugs are no longer sections. + expect(isSettingsSectionSlug("dashboard")).toBe(false); + expect(isSettingsSectionSlug("sharing")).toBe(false); expect(isSettingsSectionSlug("")).toBe(false); expect(isSettingsSectionSlug("Account")).toBe(false); // case-sensitive }); @@ -259,17 +259,9 @@ describe("", () => { } }); - it("highlights the Layout hub when the active prop is a Layout child", () => { - // The page passes `active={section}` explicitly; a child slug must - // still resolve onto the Layout hub for nav highlighting. - const html = renderShell({ - active: "dashboard", - pathname: "/settings/dashboard", - }); - const layoutActive = - /]*\baria-current="page"[^>]*\bhref="\/settings\/layout"|]*\bhref="\/settings\/layout"[^>]*\baria-current="page"/g; - expect(html.match(layoutActive)?.length ?? 0).toBe(2); - }); + // v1.37.19 — the Layout children left `SETTINGS_SECTION_SLUGS`, so no + // page can pass one as the `active` override anymore; the pathname-based + // case above is the remaining (defensive) mapping. it("no longer surfaces the per-module nav entries (folded into Darstellung, v1.25.7)", () => { // v1.25.7 — Medikamente / Stimmung / Labor / Krankheit / Vorsorge are no diff --git a/src/components/settings/about-section.tsx b/src/components/settings/about-section.tsx index 8161f39fd..5904130ed 100644 --- a/src/components/settings/about-section.tsx +++ b/src/components/settings/about-section.tsx @@ -1,6 +1,7 @@ "use client"; import { useEffect, useState } from "react"; +import Link from "next/link"; import { useQuery } from "@tanstack/react-query"; import { ArrowUpCircle, @@ -313,6 +314,21 @@ export function AboutSection() { + {/* /about carries the GeoLite2 attribution its EULA wants + reachable from the running app — this row and the /privacy + footer are its two inbound links. Same-app route, so no + external-link glyph. */} + )} diff --git a/src/components/settings/coach-prefs-section.tsx b/src/components/settings/coach-prefs-section.tsx index d18b2bd69..75d8e9404 100644 --- a/src/components/settings/coach-prefs-section.tsx +++ b/src/components/settings/coach-prefs-section.tsx @@ -170,7 +170,10 @@ export function CoachPrefsSection({ isAuthenticated }: CoachPrefsSectionProps) { - {/* Verbosity */} + {/* Verbosity. The concise tone forces "brief" server-side + (system-prompt.ts, buildPrefsPrefix), so the picker is + disabled there — showing "brief" — with a hint instead of + a live control that silently does nothing. */}
diff --git a/src/components/settings/import-panel/__tests__/import-result-view.test.tsx b/src/components/settings/import-panel/__tests__/import-result-view.test.tsx index de6506f2f..1cc073cff 100644 --- a/src/components/settings/import-panel/__tests__/import-result-view.test.tsx +++ b/src/components/settings/import-panel/__tests__/import-result-view.test.tsx @@ -107,7 +107,7 @@ describe(" — result states", () => { ); expect(html).toContain('data-outcome="success"'); expect(html).toContain("text-success"); - expect(html).toContain("Imported 4 new and updated 1."); + expect(html).toContain("Imported 4 new and updated 1"); expect(html).not.toContain("skipped"); }); diff --git a/src/components/settings/record-settings-section-gate.tsx b/src/components/settings/record-settings-section-gate.tsx index aaf697477..621112ccc 100644 --- a/src/components/settings/record-settings-section-gate.tsx +++ b/src/components/settings/record-settings-section-gate.tsx @@ -32,7 +32,6 @@ const MANAGED_RECORD_SETTINGS_FAMILY_BY_SECTION = { account: "profile", modules: "modules", notifications: "notifications", - insights: "insights", thresholds: "thresholds", coach: "coach", } as const; diff --git a/src/components/settings/section-placeholder.tsx b/src/components/settings/section-placeholder.tsx deleted file mode 100644 index eff2f4430..000000000 --- a/src/components/settings/section-placeholder.tsx +++ /dev/null @@ -1,119 +0,0 @@ -"use client"; - -/** - * `` — the temporary body of every `/settings/[section]` - * route until the matching extraction PR lands. - * - * The shell + routing surface ship in PR A2-shell so reviewers can validate - * the URL structure in isolation. The follow-up PRs (A2-account, A2-about, - * A2-ai, A2-integrations, A2-notifications, A2-rest) replace this placeholder - * with the real content for each slug, copying from - * `src/app/settings/page.legacy.tsx`. - */ - -import { - Bell, - Blocks, - Bot, - CalendarCheck, - CloudSun, - Download, - ClipboardList, - FileHeart, - FlaskConical, - Gauge, - Info, - KeyRound, - Plug, - Layers, - LayoutDashboard, - Link2, - Lock, - Pill, - Settings2, - Share2, - ShieldCheck, - SlidersHorizontal, - Smile, - Sparkles, - Thermometer, - TrendingUp, - User, - UsersRound, - type LucideIcon, -} from "lucide-react"; - -import { EmptyState } from "@/components/ui/empty-state"; -import { useTranslations } from "@/lib/i18n/context"; -import type { SettingsSectionSlug } from "./section-slugs"; - -const SLUG_ICON: Record = { - account: User, - security: ShieldCheck, - access: UsersRound, - modules: Blocks, - integrations: Link2, - sources: Layers, - notifications: Bell, - layout: LayoutDashboard, - dashboard: LayoutDashboard, - insights: TrendingUp, - medications: Pill, - mood: Smile, - labs: FlaskConical, - illness: Thermometer, - environment: CloudSun, - anamnesis: ClipboardList, - score: Gauge, - vorsorge: CalendarCheck, - thresholds: SlidersHorizontal, - ai: Sparkles, - coach: Bot, - api: KeyRound, - mcp: Plug, - gesundheitsakte: FileHeart, - sharing: Share2, - export: Download, - advanced: Settings2, - privacy: Lock, - about: Info, -}; - -export interface SectionPlaceholderProps { - slug: SettingsSectionSlug; -} - -export function SectionPlaceholder({ slug }: SectionPlaceholderProps) { - const { t } = useTranslations(); - const Icon = SLUG_ICON[slug]; - const sectionTitle = t(`settings.sections.${slug}.title`); - const sectionDescription = t(`settings.sections.${slug}.description`); - - // v1.4.33 A4 — `` is a defensive fallback for - // slugs added to SETTINGS_SECTION_SLUGS without a matching component. - // The type system blocks that path today, so the EmptyState body is - // effectively unreachable. We retire the dedicated locale key - // (`settings.sections.placeholder.coming_soon`) and inherit the - // section's own description so the guard still renders meaningful - // copy if it ever does fire — no more dead-string maintenance across - // six locale files. - return ( -
-
-

- {sectionTitle} -

-

{sectionDescription}

-
- - } - title={sectionTitle} - description={sectionDescription} - /> -
- ); -} diff --git a/src/components/settings/section-slugs.ts b/src/components/settings/section-slugs.ts index d73f5eb53..65ec120e8 100644 --- a/src/components/settings/section-slugs.ts +++ b/src/components/settings/section-slugs.ts @@ -71,13 +71,15 @@ export const SETTINGS_SECTION_SLUGS = [ // 301-redirects to `/settings/notifications#channels` (next.config.ts). "sources", "notifications", + // v1.37.19 — the per-module personalization slugs (`dashboard`, + // `insights`, `medications`, `mood`, `labs`, `illness`, `vorsorge`) and + // `sharing` left this list. Their URLs are owned by permanent redirects + // in `next.config.ts` (`/settings/` → `/settings/layout/`, + // `/settings/sharing` → `/settings/gesundheitsakte#sharing`), so the + // dynamic route could never serve them — prerendering eight unreachable + // pages and keeping eight dead component wirings. The redirects, the + // section components, and the layout-hub subpages are unchanged. "layout", - "dashboard", - "insights", - "medications", - "mood", - "labs", - "illness", // v1.25 (W-ENV) — "Umwelt" (environmental context): home location, travel // overrides, and the weather/daylight backfill. Module-gated on the opt-in // `environment` module (the nav entry only shows when it is on). @@ -92,7 +94,6 @@ export const SETTINGS_SECTION_SLUGS = [ // shown) and folding the two together is what kept the score's // composition invisible. "score", - "vorsorge", "thresholds", "ai", // v1.18.0 (S5) — `coach` gathers the Coach preference cards (disable @@ -106,13 +107,6 @@ export const SETTINGS_SECTION_SLUGS = [ // access surfaces). Not nav-gated — the card carries its own enable toggle. "mcp", "gesundheitsakte", - // v1.18.7 — `sharing` (clinician share links) sits directly after - // `gesundheitsakte`, before `export`: minting a time-boxed read-only link - // to the health record belongs next to the health-record export. Always - // available (no module gate), like account / export. The backing model, - // the `/api/share-links` routes, and the public `/c/[token]` view are - // unchanged — this restores only the owner Settings surface. - "sharing", "export", "advanced", // v1.23 — "Data & Privacy": a single surface that assembles the already- diff --git a/src/lib/onboarding/__tests__/disclaimer.test.ts b/src/lib/onboarding/__tests__/disclaimer.test.ts index d7a9cf2de..497de02f5 100644 --- a/src/lib/onboarding/__tests__/disclaimer.test.ts +++ b/src/lib/onboarding/__tests__/disclaimer.test.ts @@ -25,7 +25,10 @@ describe("isDisclaimerAcknowledgmentCurrent", () => { it("re-prompts when the stored acknowledgment is for an older version", () => { expect( - isDisclaimerAcknowledgmentCurrent("2026-06-20T08:00:00.000Z", "2025-01-01"), + isDisclaimerAcknowledgmentCurrent( + "2026-06-20T08:00:00.000Z", + "2025-01-01", + ), ).toBe(false); }); diff --git a/src/lib/record-settings/classification.ts b/src/lib/record-settings/classification.ts index 2c600d42d..0e532fd16 100644 --- a/src/lib/record-settings/classification.ts +++ b/src/lib/record-settings/classification.ts @@ -78,23 +78,15 @@ export const SETTINGS_DESTINATION_INVENTORY = { sources: UNAVAILABLE, notifications: MANAGED_GUARDIAN, layout: ADULT_SHARED_UNAVAILABLE, - dashboard: ADULT_SHARED_UNAVAILABLE, - insights: MANAGED_GUARDIAN, - medications: ADULT_SHARED_UNAVAILABLE, - mood: ADULT_SHARED_UNAVAILABLE, - labs: ADULT_SHARED_UNAVAILABLE, - illness: ADULT_SHARED_UNAVAILABLE, environment: UNAVAILABLE, anamnesis: MANAGE_WRITABLE, score: ADULT_SHARED_UNAVAILABLE, - vorsorge: ADULT_SHARED_UNAVAILABLE, thresholds: MANAGED_GUARDIAN, ai: UNAVAILABLE, coach: MANAGED_GUARDIAN, api: PERSONAL, mcp: PERSONAL, gesundheitsakte: PERSONAL, - sharing: UNAVAILABLE, export: PERSONAL, advanced: PERSONAL, privacy: PERSONAL,