diff --git a/CHANGELOG.md b/CHANGELOG.md index c47984624..fda35fdb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,18 @@ ## [Unreleased] -## [1.25.6] — 2026-06-29 — Optional HTTP geolocation provider +## [1.25.7] — 2026-06-29 — Settings, tidied + +A settings information-architecture cleanup. No schema change; every page keeps a working address (old links 301-redirect). + +### Changed + +- **Appearance** is now the single home for how each area looks and is set up: the dashboard, insights, medications, mood, labs, illness journal, and preventive-care surfaces render inline as sections there (each with its full view/sort and management cards), instead of being scattered as their own left-hand entries. A disabled module's section simply doesn't appear. The per-area pages keep their addresses and redirect into the matching Appearance section. +- **Delivery channels** (Telegram, ntfy, web push, …) now live under **Integrations** alongside the other external services, instead of under Notifications. Notifications keeps the reminder types. +- **Share links** move into **Health record**, next to the export they belong with — the standalone Sharing entry is gone. +- **Account** no longer repeats active sessions and security activity; those live under Data & Privacy. + +These changes only relocate existing surfaces — nothing was restyled, and every deep link keeps working. A focused configuration release. No schema change. diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index b172ec38c..86fc002a4 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: HealthLog API - version: 1.25.6 + version: 1.25.7 description: >- Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest. diff --git a/messages/de.json b/messages/de.json index 2fee10f23..009dc4652 100644 --- a/messages/de.json +++ b/messages/de.json @@ -4038,16 +4038,16 @@ "integrations": { "title": "Integrationen", "description": "Verbundene Dienste, Zustellkanäle und die Quellen-Priorität an einem Ort.", - "subtitle": "Datenquellen und Geräte verbinden und verwalten." + "subtitle": "Datenquellen und Geräte verbinden und verwalten.", + "channelsHeading": "Zustellkanäle", + "channelsDescription": "Wohin Erinnerungen und Hinweise zugestellt werden." }, "notifications": { "title": "Benachrichtigungen", "description": "Alle Erinnerungen an einem Ort — was wann erinnert wird und wohin es zugestellt wird.", "subtitle": "Wähle, welche Erinnerungen du erhältst und wohin sie zugestellt werden.", "remindersHeading": "Erinnerungen", - "remindersDescription": "Was du erhältst.", - "channelsHeading": "Zustellkanäle", - "channelsDescription": "Wohin Erinnerungen und Hinweise zugestellt werden." + "remindersDescription": "Was du erhältst." }, "layout": { "title": "Darstellung", @@ -4077,6 +4077,10 @@ "vorsorge": { "title": "Vorsorge", "description": "Listenansicht und Reihenfolge der Erinnerungen." + }, + "mood": { + "title": "Stimmungs-Tags", + "description": "Gruppen, Tags und archivierte Tags der Stimmungs-Auswahl." } }, "dashboard": { diff --git a/messages/en.json b/messages/en.json index 22c7fd8e5..d8129dff4 100644 --- a/messages/en.json +++ b/messages/en.json @@ -4038,16 +4038,16 @@ "integrations": { "title": "Integrations", "description": "Connected services, delivery channels, and source priority in one place.", - "subtitle": "Connect and manage your data sources and devices." + "subtitle": "Connect and manage your data sources and devices.", + "channelsHeading": "Delivery channels", + "channelsDescription": "Where reminders and alerts are delivered." }, "notifications": { "title": "Notifications", "description": "Every reminder in one place — what gets reminded and when, and where it's delivered.", "subtitle": "Choose which reminders you receive and where they're delivered.", "remindersHeading": "Reminders", - "remindersDescription": "What you receive.", - "channelsHeading": "Delivery channels", - "channelsDescription": "Where reminders and alerts are delivered." + "remindersDescription": "What you receive." }, "layout": { "title": "Appearance", @@ -4077,6 +4077,10 @@ "vorsorge": { "title": "Checkups", "description": "List view and reminder order." + }, + "mood": { + "title": "Mood tags", + "description": "Groups, tags, and archived tags for the mood picker." } }, "dashboard": { diff --git a/messages/es.json b/messages/es.json index f785cbcc6..67664efc4 100644 --- a/messages/es.json +++ b/messages/es.json @@ -4038,16 +4038,16 @@ "integrations": { "title": "Integraciones", "description": "Servicios conectados, canales de entrega y prioridad de fuentes en un solo lugar.", - "subtitle": "Conecta y gestiona tus fuentes de datos y dispositivos." + "subtitle": "Conecta y gestiona tus fuentes de datos y dispositivos.", + "channelsHeading": "Canales de entrega", + "channelsDescription": "Dónde se entregan los recordatorios y avisos." }, "notifications": { "title": "Notificaciones", "description": "Todos los recordatorios en un solo lugar: qué se recuerda y cuándo, y dónde se entrega.", "subtitle": "Elige qué recordatorios recibes y dónde se entregan.", "remindersHeading": "Recordatorios", - "remindersDescription": "Lo que recibes.", - "channelsHeading": "Canales de entrega", - "channelsDescription": "Dónde se entregan los recordatorios y avisos." + "remindersDescription": "Lo que recibes." }, "layout": { "title": "Apariencia", @@ -4077,6 +4077,10 @@ "vorsorge": { "title": "Revisiones", "description": "Vista de lista y orden de recordatorios." + }, + "mood": { + "title": "Etiquetas de ánimo", + "description": "Grupos, etiquetas y etiquetas archivadas del selector de ánimo." } }, "dashboard": { diff --git a/messages/fr.json b/messages/fr.json index 5ce57f7a1..9ee8ddf1c 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -4038,16 +4038,16 @@ "integrations": { "title": "Intégrations", "description": "Services connectés, canaux de diffusion et priorité des sources au même endroit.", - "subtitle": "Connectez et gérez vos sources de données et appareils." + "subtitle": "Connectez et gérez vos sources de données et appareils.", + "channelsHeading": "Canaux de diffusion", + "channelsDescription": "Où sont envoyés les rappels et alertes." }, "notifications": { "title": "Notifications", "description": "Tous les rappels au même endroit — quoi et quand, et où c'est envoyé.", "subtitle": "Choisissez les rappels que vous recevez et où ils sont envoyés.", "remindersHeading": "Rappels", - "remindersDescription": "Ce que vous recevez.", - "channelsHeading": "Canaux de diffusion", - "channelsDescription": "Où sont envoyés les rappels et alertes." + "remindersDescription": "Ce que vous recevez." }, "layout": { "title": "Apparence", @@ -4077,6 +4077,10 @@ "vorsorge": { "title": "Examens", "description": "Vue en liste et ordre des rappels." + }, + "mood": { + "title": "Étiquettes d’humeur", + "description": "Groupes, étiquettes et étiquettes archivées du sélecteur d’humeur." } }, "dashboard": { diff --git a/messages/it.json b/messages/it.json index 443f7feea..1754dc8dc 100644 --- a/messages/it.json +++ b/messages/it.json @@ -4038,16 +4038,16 @@ "integrations": { "title": "Integrazioni", "description": "Servizi collegati, canali di consegna e priorità delle fonti in un unico posto.", - "subtitle": "Collega e gestisci le tue fonti di dati e i dispositivi." + "subtitle": "Collega e gestisci le tue fonti di dati e i dispositivi.", + "channelsHeading": "Canali di recapito", + "channelsDescription": "Dove vengono recapitati promemoria e avvisi." }, "notifications": { "title": "Notifiche", "description": "Tutti i promemoria in un unico posto — cosa e quando, e dove vengono recapitati.", "subtitle": "Scegli quali promemoria ricevere e dove vengono recapitati.", "remindersHeading": "Promemoria", - "remindersDescription": "Ciò che ricevi.", - "channelsHeading": "Canali di recapito", - "channelsDescription": "Dove vengono recapitati promemoria e avvisi." + "remindersDescription": "Ciò che ricevi." }, "layout": { "title": "Aspetto", @@ -4077,6 +4077,10 @@ "vorsorge": { "title": "Controlli", "description": "Vista elenco e ordine dei promemoria." + }, + "mood": { + "title": "Tag dell’umore", + "description": "Gruppi, tag e tag archiviati del selettore dell’umore." } }, "dashboard": { diff --git a/messages/pl.json b/messages/pl.json index b3a3942a6..462f42ac3 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -4038,16 +4038,16 @@ "integrations": { "title": "Integracje", "description": "Połączone usługi, kanały dostarczania i priorytet źródeł w jednym miejscu.", - "subtitle": "Połącz i zarządzaj źródłami danych oraz urządzeniami." + "subtitle": "Połącz i zarządzaj źródłami danych oraz urządzeniami.", + "channelsHeading": "Kanały dostarczania", + "channelsDescription": "Gdzie dostarczane są przypomnienia i powiadomienia." }, "notifications": { "title": "Powiadomienia", "description": "Wszystkie przypomnienia w jednym miejscu — co i kiedy oraz gdzie są dostarczane.", "subtitle": "Wybierz, które przypomnienia otrzymujesz i gdzie są dostarczane.", "remindersHeading": "Przypomnienia", - "remindersDescription": "Co otrzymujesz.", - "channelsHeading": "Kanały dostarczania", - "channelsDescription": "Gdzie dostarczane są przypomnienia i powiadomienia." + "remindersDescription": "Co otrzymujesz." }, "layout": { "title": "Wygląd", @@ -4077,6 +4077,10 @@ "vorsorge": { "title": "Badania kontrolne", "description": "Widok listy i kolejność przypomnień." + }, + "mood": { + "title": "Tagi nastroju", + "description": "Grupy, tagi i zarchiwizowane tagi w wyborze nastroju." } }, "dashboard": { diff --git a/next.config.ts b/next.config.ts index 219619fea..ce04e9e87 100644 --- a/next.config.ts +++ b/next.config.ts @@ -134,15 +134,52 @@ const nextConfig: NextConfig = { destination: "/settings/notifications", permanent: true, }, - // v1.25.3 — the standalone delivery-channels page folded into - // Notifications as an in-page group. The channels content now lives - // under the `#channels` anchor on the Notifications surface, so the old - // URL 301-redirects there (mirroring the `/settings/reminders` precedent - // above). The per-channel anchors (`#telegram`, `#ntfy`, …) inside the - // panel keep resolving. + // v1.25.7 — delivery channels live under Settings → Integrationen (they + // are delivery providers, the same family as the connected services). + // The channels content sits under the `#channels` anchor there, so the + // old URL 301-redirects to it. The per-channel anchors (`#telegram`, + // `#ntfy`, …) inside the panel keep resolving. { source: "/settings/channels", - destination: "/settings/notifications#channels", + destination: "/settings/integrations#channels", + permanent: true, + }, + // v1.25.7 — the per-module view/sort/order surfaces (Medikamente, + // Stimmung, Labor, Krankheit, Vorsorge) folded into "Darstellung" + // (`/settings/layout`) as inline anchored sections. Their standalone + // routes 301-redirect to the matching anchor so bookmarks, the PWA's + // cached navigation, and the per-module page-header cogs keep resolving. + { + source: "/settings/medications", + destination: "/settings/layout#medications", + permanent: true, + }, + { + source: "/settings/mood", + destination: "/settings/layout#mood", + permanent: true, + }, + { + source: "/settings/labs", + destination: "/settings/layout#labs", + permanent: true, + }, + { + source: "/settings/illness", + destination: "/settings/layout#illness", + permanent: true, + }, + { + source: "/settings/vorsorge", + destination: "/settings/layout#vorsorge", + permanent: true, + }, + // v1.25.7 — clinician share links fold into the Gesundheitsakte section + // as a labelled "Sharing" group; the old standalone route 301-redirects + // to the anchor there. + { + source: "/settings/sharing", + destination: "/settings/gesundheitsakte#sharing", permanent: true, }, // v1.22.0 — the preventive-care surface moved from the German slug diff --git a/package.json b/package.json index c596973bc..898d0e8ba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "healthlog", - "version": "1.25.6", + "version": "1.25.7", "description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.", "license": "PolyForm-Noncommercial-1.0.0", "homepage": "https://healthlog.dev", diff --git a/public/sw.js b/public/sw.js index ad2cb3618..3e9934830 100644 --- a/public/sw.js +++ b/public/sw.js @@ -36,7 +36,7 @@ try { // v1.4.38.4 → v1.4.42. Do not hand-edit; bump `package.json` and rebuild. const CACHE_VERSION = (typeof self !== "undefined" && self.__APP_VERSION__) || - /* @sw-version-fallback */ "v1.25.6"; + /* @sw-version-fallback */ "v1.25.7"; const STATIC_CACHE = `healthlog-static-${CACHE_VERSION}`; const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`; // v1.18.6 — read-only data cache for a curated allowlist of safe GET `/api/*` diff --git a/src/components/settings/__tests__/layout-section.test.tsx b/src/components/settings/__tests__/layout-section.test.tsx index 79669842b..a13ac05f4 100644 --- a/src/components/settings/__tests__/layout-section.test.tsx +++ b/src/components/settings/__tests__/layout-section.test.tsx @@ -1,22 +1,57 @@ /** - * v1.17.1 (F-2) — the "Appearance" hub (slug stays `layout`). + * v1.25.7 — the "Appearance" home (slug stays `layout`). * - * The hub is the single front door for every view/arrangement surface. It must - * link to each with consistent framing so "how my app looks" reads as one home - * rather than scattered settings sections. - * - * v1.25.3 — the hub widened from 2 links (dashboard + insights) to 6: it now - * also deep-links to the view/sort/order cards of medications, labs, the - * illness journal, and checkups. Those cards stay on their own module pages; - * the hub only indexes them via anchor deep-links. + * The hub stopped being a link list: it now composes the dashboard, insights, + * and every tracking module's settings inline as stacked, anchored sections. + * Each module section keeps its module-gate (fail-open `!== false`), so a + * disabled module's section does not render. These tests pin the composition + * contract — anchors, headings, and gating — and mock the child section + * components (each has its own dedicated test) so the render stays a pure + * composition smoke. */ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { renderToStaticMarkup } from "react-dom/server"; +// Mutable modules ref so each render can pick a different gate map. +const authRef: { modules?: Record } = {}; +vi.mock("@/hooks/use-auth", () => ({ + useAuth: () => ({ + user: { id: "u1", username: "t", role: "USER", modules: authRef.modules }, + isAuthenticated: true, + isLoading: false, + refetch: vi.fn(), + }), +})); + +// Each composed surface has its own test; stub them at the import boundary so +// this test exercises only the LayoutSection composition (anchors + gating). +vi.mock("@/components/settings/dashboard-section", () => ({ + DashboardSection: () =>
, +})); +vi.mock("@/components/settings/insights-section", () => ({ + InsightsSection: () =>
, +})); +vi.mock("@/components/settings/medications-section", () => ({ + MedicationsSection: () =>
, +})); +vi.mock("@/components/settings/mood-section", () => ({ + MoodSection: () =>
, +})); +vi.mock("@/components/settings/labs-section", () => ({ + LabsSection: () =>
, +})); +vi.mock("@/components/settings/illness-section", () => ({ + IllnessSection: () =>
, +})); +vi.mock("@/components/settings/vorsorge-section", () => ({ + VorsorgeSection: () =>
, +})); + import { I18nProvider } from "@/lib/i18n/context"; import { LayoutSection } from "../layout-section"; -function render(locale: "en" | "de" = "en") { +function render(modules?: Record, locale: "en" | "de" = "en") { + authRef.modules = modules; return renderToStaticMarkup( @@ -25,32 +60,57 @@ function render(locale: "en" | "de" = "en") { } describe("", () => { - it("links to every view surface it indexes (anchor deep-links)", () => { - const html = render(); - for (const href of [ - "/settings/dashboard", - "/settings/insights", - "/settings/medications#medications-view", - "/settings/labs#labs-view", - "/settings/illness#illness-view", - "/settings/vorsorge#vorsorge-view", + it("stacks every surface inline with a stable anchor id (fail-open default)", () => { + const html = render(undefined); + for (const id of [ + "dashboard", + "insights", + "medications", + "mood", + "labs", + "illness", + "vorsorge", ]) { - expect(html).toContain(`href="${href}"`); + expect(html).toContain(`id="${id}"`); + expect(html).toContain(`data-testid="body-${id}"`); } + // It is no longer a link hub — no `/settings/*` cards. + expect(html).not.toContain('href="/settings/'); }); - it("renders one card per indexed surface (6 links)", () => { + it("hides a module's section when that module is disabled", () => { + const html = render({ medications: false, labs: false }); + expect(html).not.toContain('data-testid="body-medications"'); + expect(html).not.toContain('id="medications"'); + expect(html).not.toContain('data-testid="body-labs"'); + expect(html).not.toContain('id="labs"'); + // Other modules unaffected; Vorsorge is never gated. + expect(html).toContain('data-testid="body-mood"'); + expect(html).toContain('data-testid="body-vorsorge"'); + }); + + it("always renders dashboard, insights, and vorsorge (no module gate)", () => { + const html = render({ + medications: false, + mood: false, + labs: false, + illness: false, + }); + expect(html).toContain('data-testid="body-dashboard"'); + expect(html).toContain('data-testid="body-insights"'); + expect(html).toContain('data-testid="body-vorsorge"'); + expect(html).not.toContain('data-testid="body-mood"'); + }); + + it("resolves its section headings via i18n (no raw keys leak)", () => { const html = render(); - // Pin the link count: every hub card is an ``. - const links = html.match(/href="\/settings\//g) ?? []; - expect(links.length).toBe(6); - expect(html).toContain("Dashboard"); - expect(html).toContain("Insights"); + expect(html).toContain("Medications"); + expect(html).toContain("Labs"); + expect(html).not.toContain("settings.sections.layout."); }); - it("resolves its copy in German too", () => { - const html = render("de"); - // The hub body is the link list; the German module labels resolve. + it("resolves the headings in German too", () => { + const html = render(undefined, "de"); expect(html).toContain("Medikamente"); expect(html).toContain("Labor"); expect(html).not.toContain("settings.sections.layout."); diff --git a/src/components/settings/__tests__/sections.test.tsx b/src/components/settings/__tests__/sections.test.tsx index 183ef2bf7..b4363f3d2 100644 --- a/src/components/settings/__tests__/sections.test.tsx +++ b/src/components/settings/__tests__/sections.test.tsx @@ -182,13 +182,18 @@ describe("settings sections — SSR smoke", () => { expect(html).toContain("settings-section-ai-title"); }); - it(" renders Withings card title", () => { + it(" renders Withings card + the delivery-channels group", () => { const html = renderFramed("integrations", ); expect(html).toContain("Integrations"); expect(html).toContain("Withings"); + // v1.25.7 — delivery channels moved here from Notifications: the + // `#channels` anchor and the embedded channels panel both render. + expect(html).toContain('id="channels"'); + expect(html).toContain('data-slot="notification-channels-panel"'); + expect(html).toContain("Delivery channels"); }); - it(" renders the reminders + delivery-channels groups", () => { + it(" renders the reminders group only (channels moved out, v1.25.7)", () => { const html = renderFramed("notifications", ); // v1.18.6 (W9) — the visible heading comes from the frame; the proactive // Coach nudge moved to Settings → Coach, so it no longer renders here. @@ -201,12 +206,11 @@ describe("settings sections — SSR smoke", () => { expect(html).toContain('id="mood-reminder"'); expect(html).toContain('id="low-stock"'); expect(html).not.toContain('id="coach-nudge"'); - // v1.25.3 — the delivery-channels group is now part of this surface: the - // `#channels` anchor and the embedded channels panel both render. - expect(html).toContain('id="channels"'); - expect(html).toContain('data-slot="notification-channels-panel"'); + // v1.25.7 — delivery channels moved to Settings → Integrationen, so the + // channels group + panel no longer render on this surface. + expect(html).not.toContain('id="channels"'); + expect(html).not.toContain('data-slot="notification-channels-panel"'); expect(html).toContain("Reminders"); - expect(html).toContain("Delivery channels"); // No raw i18n key leaks past the provider. expect(html).not.toContain("settings.sections.notifications."); }); diff --git a/src/components/settings/__tests__/settings-shell.test.tsx b/src/components/settings/__tests__/settings-shell.test.tsx index e2142a1ec..a65e9a258 100644 --- a/src/components/settings/__tests__/settings-shell.test.tsx +++ b/src/components/settings/__tests__/settings-shell.test.tsx @@ -226,26 +226,26 @@ describe("", () => { expect(html.match(layoutActive)?.length ?? 0).toBe(2); }); - it("module-gates the Stimmung entry off `user.modules.mood` (v1.18.0 S5)", () => { - // Fail-open default: mood undefined → entry shown. - expect(renderShell({ active: "account" })).toContain( - 'href="/settings/mood"', + 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 + // longer standalone left-nav entries: their settings render inline inside + // the "Darstellung" hub, gated on the same module key there. The shell + // never emits a sidebar link for them regardless of the module map. + for (const slug of ["medications", "mood", "labs", "illness", "vorsorge"]) { + expect(renderShell({ active: "account" })).not.toContain( + `href="/settings/${slug}"`, + ); + // Even with the module explicitly enabled, no sidebar entry appears. + expect( + renderShell({ active: "account", modules: { [slug]: true } }), + ).not.toContain(`href="/settings/${slug}"`); + } + }); + + it("no longer surfaces the Sharing nav entry (folded into Gesundheitsakte, v1.25.7)", () => { + expect(renderShell({ active: "account" })).not.toContain( + 'href="/settings/sharing"', ); - // Explicitly disabled → entry hidden. - const disabled = renderShell({ - active: "account", - modules: { mood: false }, - }); - expect(disabled).not.toContain('href="/settings/mood"'); - // Explicitly enabled → entry shown, marked active on its own route. - const enabled = renderShell({ - active: "mood", - pathname: "/settings/mood", - modules: { mood: true }, - }); - const moodActive = - /]*\baria-current="page"[^>]*\bhref="\/settings\/mood"|]*\bhref="\/settings\/mood"[^>]*\baria-current="page"/g; - expect(enabled.match(moodActive)?.length ?? 0).toBe(2); }); it("module-gates the Coach entry off `user.modules.coach` (v1.18.0 S5)", () => { @@ -305,10 +305,13 @@ describe("", () => { // left-side entry; Sources keeps its own. expect(html).not.toContain('href="/settings/channels"'); expect(html).toContain('href="/settings/sources"'); - // v1.18.0 (S5) — Medications (Medikamente) and Mood (Stimmung, gated; - // fail-open mock shows both) are their own nav entries now. - expect(html).toContain('href="/settings/medications"'); - expect(html).toContain('href="/settings/mood"'); + // v1.25.7 — Medikamente / Stimmung (and Labor / Krankheit / Vorsorge) + // folded into "Darstellung" as inline sections, so they no longer have a + // standalone left-nav entry. + expect(html).not.toContain('href="/settings/medications"'); + expect(html).not.toContain('href="/settings/mood"'); + // v1.25.7 — Sharing folded into the Gesundheitsakte section. + expect(html).not.toContain('href="/settings/sharing"'); // The ampersand is HTML-escaped by React SSR — assert on the encoded // form so we don't accidentally match a parser that double-escapes. expect(html).toContain("API & Tokens"); @@ -345,9 +348,10 @@ describe("", () => { // v1.25.3 — Channels ("Kanäle") folded into Notifications; Sources keeps // its own left-side entry. expect(html).not.toContain('href="/settings/channels"'); - // v1.18.0 (S5) — Medications + Mood are their own nav entries now. - expect(html).toContain('href="/settings/medications"'); - expect(html).toContain('href="/settings/mood"'); + // v1.25.7 — Medikamente + Stimmung folded into "Darstellung", so they no + // longer have their own nav entries. + expect(html).not.toContain('href="/settings/medications"'); + expect(html).not.toContain('href="/settings/mood"'); // Targets keeps its German nav entry ("Zielwerte"). expect(html).toContain("Zielwerte"); // v1.18.6 (W9) — the AI section is named "KI-Anbieter" in German: the diff --git a/src/components/settings/account-section/index.tsx b/src/components/settings/account-section/index.tsx index 16d30e392..c51307fc6 100644 --- a/src/components/settings/account-section/index.tsx +++ b/src/components/settings/account-section/index.tsx @@ -37,8 +37,6 @@ import { type StatusMessage, } from "./account-section-utils"; import { AvatarSection } from "./avatar-section"; -import { SecuritySessionsCard } from "@/components/settings/security-sessions-card"; -import { SecurityActivityCard } from "@/components/settings/security-activity-card"; export { resolveInitialTimezone } from "./account-section-utils"; @@ -498,11 +496,9 @@ export function AccountSection() {
- {/* v1.23 — active-session management (issue #64) + the security-activity - feed. Both sit in the account/security block under the credential - controls above. */} - - + {/* v1.25.7 — active-session management + the security-activity feed + live only under Settings → Data & Privacy now; the duplicate cards + that used to sit here were removed so each surfaces in one place. */} {/* v1.18.1 (D1) — the "Tour neu starten" card moved to Settings → Erweitert. It is a maintenance / reset action, not a profile or diff --git a/src/components/settings/gesundheitsakte-section.tsx b/src/components/settings/gesundheitsakte-section.tsx index df924a4b5..ea13de7bd 100644 --- a/src/components/settings/gesundheitsakte-section.tsx +++ b/src/components/settings/gesundheitsakte-section.tsx @@ -3,12 +3,10 @@ /** * v1.18.0 (S5) — Settings → Gesundheitsakte (health record). * - * The full health-record export was the hero panel at the top of the - * Export & Import section. It is a distinct concept — a complete, - * clinician-ready record (PDF + FHIR R4 + a combined zip package) — and - * earns its own top-level settings home rather than sharing the page with - * the generic CSV / JSON data-out paths. Export & Import now keeps only - * the generic data export/import surfaces. + * The full health-record export is a distinct concept — a complete, + * clinician-ready record (PDF + FHIR R4 + a combined zip package) — and earns + * its own top-level settings home rather than sharing the page with the + * generic CSV / JSON data-out paths. * * The `doctorReport` module governs the data layer, not the Settings * entry-point: as of v1.18.6.1 the nav entry always shows (the health record @@ -17,13 +15,40 @@ * enforcement — it refuses with a 403 `module.disabled` envelope when the * account has opted out, so a disabled doctor-report surface cannot be * reached over a Bearer token either. + * + * v1.25.7 — the clinician share-link surface (formerly the standalone + * "Freigabe" section) folds in here as a labelled "Sharing" group below the + * export panel: a time-boxed read-only link is a sharing face of the SAME + * health record. `/settings/sharing` 301-redirects to + * `/settings/gesundheitsakte#sharing`. The backing model, the + * `/api/share-links` routes, and the public `/c/[token]` view are unchanged. */ +import { useTranslations } from "@/lib/i18n/context"; import { HealthRecordExportPanel } from "@/components/settings/health-record-export-panel"; +import { SharingSection } from "@/components/settings/sharing-section"; export function GesundheitsakteSection() { - // v1.18.6 (W9) — the visible heading + subtitle and the module tour-replay - // trigger now live in the shared `` in the route; the - // body is the health-record export panel only. - return ; + const { t } = useTranslations(); + + // v1.18.6 (W9) — the visible page heading + subtitle come from the shared + // `` in the route; the export panel is the primary + // surface, with the share-links group sequenced below it. + return ( +
+ + +
+
+

+ {t("settings.sections.sharing.title")} +

+

+ {t("settings.sections.sharing.subtitle")} +

+
+ +
+
+ ); } diff --git a/src/components/settings/integrations-section.tsx b/src/components/settings/integrations-section.tsx index ef21a48bf..6c5fa772b 100644 --- a/src/components/settings/integrations-section.tsx +++ b/src/components/settings/integrations-section.tsx @@ -3,19 +3,27 @@ /** * `` — Settings → Integrationen. * - * v1.18.1 (D4) — the three sub-tabs (Connections / Channels / Sources) were - * split into three separate left-side settings entries. The Connections - * content IS the Integrationen page now: clicking Integrationen shows the - * OAuth + device data sources (Withings / WHOOP / Fitbit / Polar / Oura / - * Nightscout) directly, with no tab strip. Channels (delivery providers) and - * Sources (source weighting) each got their own entry — see - * `channels-section.tsx` and the standalone `sources` route. + * v1.18.1 (D4) — the Connections content IS the Integrationen page: clicking + * Integrationen shows the OAuth + device data sources (Withings / WHOOP / + * Fitbit / Polar / Oura / Nightscout) directly, with no tab strip. + * + * v1.25.7 — the delivery CHANNELS (Telegram / ntfy / Web Push / Webhook / + * Email) move back here from Notifications as a labelled "Delivery channels" + * group below the connections. Channels are delivery providers — the same + * conceptual family as Withings / WHOOP / a connected device — not reminder + * preferences, so they belong with the other integrations. Notifications keeps + * only the reminder-TYPE content. The group carries `id="channels"` so + * `/settings/channels` 301-redirects to `/settings/integrations#channels`, and + * the per-channel anchors (`#telegram`, `#ntfy`, …) inside the panel keep + * working. * * `parseOAuthOutcome` / `oauthReasonKey` are re-exported from * `connections-panel.tsx` so existing unit-test imports keep resolving. */ +import { useTranslations } from "@/lib/i18n/context"; import { ConnectionsPanel } from "@/components/settings/integrations/connections-panel"; +import { NotificationChannelsPanel } from "@/components/settings/integrations/notification-channels-panel"; export { parseOAuthOutcome, @@ -23,8 +31,30 @@ export { } from "@/components/settings/integrations/connections-panel"; export function IntegrationsSection() { - // v1.18.6 (W9) — the visible heading + subtitle and the module tour-replay - // trigger now live in the shared `` in the route; the - // body is the connections panel only. - return ; + const { t } = useTranslations(); + + // v1.18.6 (W9) — the visible page heading + subtitle come from the shared + // `` in the route; the connections panel is the + // primary surface, with the delivery-channels group sequenced below it. + return ( +
+ + + {/* Delivery channels ("where it's delivered"). The existing channels + panel, unchanged. `id="channels"` anchors the group for the + `/settings/channels` redirect; the per-channel anchors stay inside + the panel. */} +
+
+

+ {t("settings.sections.integrations.channelsHeading")} +

+

+ {t("settings.sections.integrations.channelsDescription")} +

+
+ +
+
+ ); } diff --git a/src/components/settings/layout-section.tsx b/src/components/settings/layout-section.tsx index 1eb01308c..f165a5419 100644 --- a/src/components/settings/layout-section.tsx +++ b/src/components/settings/layout-section.tsx @@ -1,123 +1,146 @@ "use client"; -import Link from "next/link"; -import { - CalendarCheck, - ChevronRight, - FlaskConical, - LayoutDashboard, - Pill, - Thermometer, - TrendingUp, - type LucideIcon, -} from "lucide-react"; - +import { useAuth } from "@/hooks/use-auth"; import { useTranslations } from "@/lib/i18n/context"; -import { SettingsCard } from "./settings-card"; +import type { ModuleKey } from "@/lib/modules/registry"; +import { DashboardSection } from "./dashboard-section"; +import { InsightsSection } from "./insights-section"; +import { MedicationsSection } from "./medications-section"; +import { MoodSection } from "./mood-section"; +import { LabsSection } from "./labs-section"; +import { IllnessSection } from "./illness-section"; +import { VorsorgeSection } from "./vorsorge-section"; /** * v1.17.1 (F-2) — the "Appearance" home (slug stays `layout`). * * "How my app looks and is arranged" was scattered across several unrelated * settings sections with no shared framing. This section is the single front - * door: it presents the view/arrangement surfaces as one consistent set of - * cards and links to each editor, which keep their own routes, their own - * cards, and their mutation flows intact — the hub indexes them, it does not - * own them. + * door for every view/arrangement surface. * - * v1.25.3 — the hub widens from the dashboard + insights arrangement editors - * to the full set of view surfaces: it now also deep-links to the view/sort/ - * order cards of medications, labs, the illness journal, and checkups. Each - * of those modules keeps its view cards on its own settings page; the links - * land on the existing card anchors (`#medications-view`, `#labs-view`, - * `#illness-view`, `#vorsorge-view`). Data/behaviour settings (biomarker - * catalog, local OCR, injection-site exclusions, per-reminder toggles, the - * mood tag/group catalog) stay on their module pages and are NOT indexed here. + * v1.25.7 — the hub stops being a link list and becomes the page itself: the + * dashboard, insights, and every tracking module's settings render inline as + * stacked, labelled sections, composing the EXISTING section components + * verbatim. The per-module settings pages are no longer standalone left-nav + * entries — their routes 301-redirect to the matching anchor here + * (`/settings/medications` → `/settings/layout#medications`, …). Each module + * section keeps its module-gate: a disabled module's section does not render, + * read from the resolved `useAuth().user.modules` map (the same map the nav, + * the Modules hub, and the Insights pills gate off). The gate fails OPEN + * (`!== false`) so a stale `/me` payload never blanks a section. Vorsorge + * (preventive-care reminders) is not a toggleable module, so it always shows. + * + * Each section carries a stable anchor id (`#dashboard`, `#insights`, + * `#medications`, `#mood`, `#labs`, `#illness`, `#vorsorge`) so deep links and + * the per-module page-header cogs land on the right block. The inner card + * anchors inside each module section (`#medications-view`, `#labs-view`, …) + * keep working too. + * + * Visual rhythm mirrors the multi-group Notifications surface 1:1 — outer + * `space-y-10`, each group a `
` with a `space-y-0.5` heading block and + * the section body below. No new card chrome is introduced; every card paints + * through its own existing component unchanged. */ -interface LayoutLink { - href: string; - icon: LucideIcon; +interface LayoutGroup { + /** Anchor id + redirect target (`/settings/layout#`). */ + id: string; + /** i18n key under `settings.sections.layout..title`. */ titleKey: string; + /** i18n key under `settings.sections.layout..description`. */ descriptionKey: string; + Body: () => React.JSX.Element | null; + /** + * When set, the group renders only while the module is enabled (fail-open, + * `!== false`). Omitted = always shown. + */ + moduleGate?: ModuleKey; } -const LAYOUT_LINKS: ReadonlyArray = [ +const LAYOUT_GROUPS: ReadonlyArray = [ { - href: "/settings/dashboard", - icon: LayoutDashboard, + id: "dashboard", titleKey: "settings.sections.layout.dashboard.title", descriptionKey: "settings.sections.layout.dashboard.description", + Body: DashboardSection, }, { - href: "/settings/insights", - icon: TrendingUp, + id: "insights", titleKey: "settings.sections.layout.insights.title", descriptionKey: "settings.sections.layout.insights.description", + Body: InsightsSection, }, - // v1.25.3 — view/sort/order surfaces of the tracking modules. Each link - // deep-links to the module's existing view card anchor; the cards stay on - // their own settings page. The module routes resolve regardless of the - // module's enabled state, so the link never dead-ends. { - href: "/settings/medications#medications-view", - icon: Pill, + id: "medications", titleKey: "settings.sections.layout.medications.title", descriptionKey: "settings.sections.layout.medications.description", + Body: MedicationsSection, + moduleGate: "medications", }, { - href: "/settings/labs#labs-view", - icon: FlaskConical, + id: "mood", + titleKey: "settings.sections.layout.mood.title", + descriptionKey: "settings.sections.layout.mood.description", + Body: MoodSection, + moduleGate: "mood", + }, + { + id: "labs", titleKey: "settings.sections.layout.labs.title", descriptionKey: "settings.sections.layout.labs.description", + Body: LabsSection, + moduleGate: "labs", }, { - href: "/settings/illness#illness-view", - icon: Thermometer, + id: "illness", titleKey: "settings.sections.layout.illness.title", descriptionKey: "settings.sections.layout.illness.description", + Body: IllnessSection, + moduleGate: "illness", }, { - href: "/settings/vorsorge#vorsorge-view", - icon: CalendarCheck, + id: "vorsorge", titleKey: "settings.sections.layout.vorsorge.title", descriptionKey: "settings.sections.layout.vorsorge.description", + Body: VorsorgeSection, }, ]; export function LayoutSection() { const { t } = useTranslations(); + const { user } = useAuth(); - // The visible heading + subtitle are painted by `SettingsShell` from the - // section slug; this body is the hub link list. + // v1.25.7 — per-module gating. Fail OPEN (`!== false`): a missing key, or a + // not-yet-resolved `/me` payload, reads as enabled so a section never + // silently disappears. Groups with no `moduleGate` always render. + const modules = user?.modules; + const visibleGroups = LAYOUT_GROUPS.filter( + (group) => !group.moduleGate || modules?.[group.moduleGate] !== false, + ); + + // The visible page heading + subtitle are painted by `SettingsShell` from + // the section slug; this body stacks the per-surface sections below it. return ( -
    - {LAYOUT_LINKS.map((link) => { - const Icon = link.icon; +
    + {visibleGroups.map((group) => { + const { Body } = group; return ( -
  • - - -
  • +
    +
    +

    + {t(group.titleKey)} +

    +

    + {t(group.descriptionKey)} +

    +
    + +
    ); })} -
+
); } diff --git a/src/components/settings/notifications-section.tsx b/src/components/settings/notifications-section.tsx index 28bb7fb1a..5e0eba377 100644 --- a/src/components/settings/notifications-section.tsx +++ b/src/components/settings/notifications-section.tsx @@ -4,7 +4,6 @@ import { useAuth } from "@/hooks/use-auth"; import { useTranslations } from "@/lib/i18n/context"; import { LowStockCard } from "@/components/settings/low-stock-card"; import { MoodReminderCard } from "@/components/settings/mood-reminder-card"; -import { NotificationChannelsPanel } from "@/components/settings/integrations/notification-channels-panel"; /** * `` — Settings → Notifications ("Benachrichtigungen"). @@ -21,15 +20,10 @@ import { NotificationChannelsPanel } from "@/components/settings/integrations/no * (a toggleable fail-open module since D3, so a missing key reads as enabled * and a stale `/me` payload never blanks a card). * - * v1.25.3 — the standalone "Channels" entry folds back in here. The page is - * now one Notifications surface with two sequential, labelled groups, ordered - * the way a user reasons about it: **Reminders** ("what you receive") first, - * then **Delivery channels** ("where it's delivered") = the existing - * ``. This is two distinct-but-related concepts - * sequenced under one roof, not one concept split top/bottom — the - * channels group carries `id="channels"` so `/settings/channels` can - * 301-redirect to `/settings/notifications#channels`, and the per-channel - * anchors (`#telegram`, `#ntfy`, …) inside the panel keep working. + * v1.25.7 — delivery channels move to Settings → Integrationen (channels are + * delivery providers, the same family as the connected services). This screen + * keeps ONLY the reminder-TYPE content; `/settings/channels` 301-redirects to + * `/settings/integrations#channels`. * * v1.18.6 (W9) — the page heading + subtitle come from the shared * ``. The proactive-Coach nudge card lives under @@ -77,21 +71,6 @@ export function NotificationsSection() { ) : null} ) : null} - - {/* Group 2 — Delivery channels ("where it's delivered"). The existing - channels panel, unchanged. `id="channels"` anchors the group for the - `/settings/channels` redirect. */} -
-
-

- {t("settings.sections.notifications.channelsHeading")} -

-

- {t("settings.sections.notifications.channelsDescription")} -

-
- -
); } diff --git a/src/components/settings/settings-shell.tsx b/src/components/settings/settings-shell.tsx index cd20feaa5..b4a73e6f8 100644 --- a/src/components/settings/settings-shell.tsx +++ b/src/components/settings/settings-shell.tsx @@ -21,12 +21,10 @@ import { Bell, Blocks, Bot, - CalendarCheck, ClipboardList, CloudSun, Download, FileHeart, - FlaskConical, Info, KeyRound, Plug, @@ -34,14 +32,10 @@ import { Link2, Lock, Palette, - Pill, Settings2, - Share2, ShieldCheck, SlidersHorizontal, - Smile, Sparkles, - Thermometer, User, type LucideIcon, } from "lucide-react"; @@ -147,49 +141,16 @@ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [ // home. v1.25.3 — renamed to "Appearance"/"Darstellung" and widened into the // index for every module's view/sort/order surface. The slug stays `layout` // so every route + deep link is untouched. + // v1.25.7 — "Darstellung" (Appearance) absorbs the per-module view/sort/ + // order surfaces. The standalone Medikamente / Stimmung / Labor / + // Krankheit / Vorsorge nav entries are gone: each module's full settings + // now render inline inside this section, gated on the same module key. The + // old routes 301-redirect to `/settings/layout#` (next.config.ts). { slug: "layout", titleKey: "settings.sections.layout.title", icon: Palette, }, - // v1.18.0 (S5) — Medikamente: medication-specific settings (list view + - // order + injection sites). Was a hidden child of the Layout hub. - // v1.18.1 (D3) — medications graduated from a CORE domain to a toggleable - // module, so this entry now hides when the account turns the module off - // (the medication data routes stay live; this only hides the surface). - { - slug: "medications", - titleKey: "settings.sections.medications.title", - icon: Pill, - moduleGate: "medications", - }, - // v1.18.0 (S5) — Stimmung: the mood-tag management surface gets its own - // nav entry, shown only when the mood module is enabled. Was a hidden - // child of the Layout hub. - { - slug: "mood", - titleKey: "settings.sections.mood.title", - icon: Smile, - moduleGate: "mood", - }, - // v1.18.7 — Labor: the labs customise surface (view, sort, biomarkers) - // becomes a first-class Settings section, gated on the `labs` module. Was - // a standalone `ModuleSettingsFrame` page reached from the /labs wrench. - { - slug: "labs", - titleKey: "settings.sections.labs.title", - icon: FlaskConical, - moduleGate: "labs", - }, - // v1.18.7 — Krankheitstagebuch: the illness-journal customise surface - // (view + order of episodes), gated on the `illness` module. Was a - // standalone `ModuleSettingsFrame` page reached from the /illness wrench. - { - slug: "illness", - titleKey: "settings.sections.illness.title", - icon: Thermometer, - moduleGate: "illness", - }, // v1.25 (W-ENV) — Environmental context: home location, travel overrides, and // the weather/daylight backfill. Module-gated on the opt-in `environment` // module, so the entry only appears once the user turns it on. @@ -208,16 +169,9 @@ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [ titleKey: "settings.sections.anamnesis.title", icon: ClipboardList, }, - // v1.18.7 — Vorsorge: the preventive-care reminders customise surface - // (view, order, individual reminders). NOT module-gated — preventive-care - // reminders are not a toggleable module — so the entry is always shown. - // Was a standalone `ModuleSettingsFrame` page reached from the /vorsorge - // wrench. - { - slug: "vorsorge", - titleKey: "settings.sections.vorsorge.title", - icon: CalendarCheck, - }, + // v1.25.7 — Vorsorge (preventive-care reminders) folded into "Darstellung" + // alongside the other tracking modules; `/settings/vorsorge` 301-redirects + // to `/settings/layout#vorsorge` (next.config.ts). { slug: "thresholds", titleKey: "settings.sections.thresholds.title", @@ -255,15 +209,11 @@ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [ titleKey: "settings.sections.gesundheitsakte.title", icon: FileHeart, }, - // v1.18.7 — clinician share links sit next to the health record: minting a - // time-boxed read-only link is a sharing face of the same data. Not - // module-gated; the link surface is always available (the public - // `/c/[token]` view and the `/api/share-links` lifecycle are unchanged). - { - slug: "sharing", - titleKey: "settings.sections.sharing.title", - icon: Share2, - }, + // v1.25.7 — clinician share links fold into the Gesundheitsakte section as a + // labelled "Sharing" group: minting a time-boxed read-only link is a sharing + // face of the same data. `/settings/sharing` 301-redirects to + // `/settings/gesundheitsakte#sharing` (next.config.ts). The public + // `/c/[token]` view and the `/api/share-links` lifecycle are unchanged. { slug: "export", titleKey: "settings.sections.export.title", @@ -329,6 +279,14 @@ export interface SettingsShellProps { const LAYOUT_CHILD_SLUGS: ReadonlySet = new Set([ "dashboard", "insights", + // v1.25.7 — the per-module view/sort/order surfaces moved into the + // "Darstellung" hub. Their routes 301-redirect to `/settings/layout#`, + // but if one is ever reached directly the nav still highlights the hub. + "medications", + "mood", + "labs", + "illness", + "vorsorge", ]); /** Map a Layout child editor onto the Layout hub for nav highlighting. */