mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
Merge pull request #389 from MBombeck/release/v1.25.7
Release v1.25.7 — settings, tidied
This commit is contained in:
20 files changed
+438
-287
No files matched your search
+12
-1
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
+8
-4
@@ -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": {
|
||||
|
||||
+8
-4
@@ -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": {
|
||||
|
||||
+8
-4
@@ -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": {
|
||||
|
||||
+8
-4
@@ -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": {
|
||||
|
||||
+8
-4
@@ -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": {
|
||||
|
||||
+8
-4
@@ -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": {
|
||||
|
||||
+44
-7
@@ -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
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
+1
-1
@@ -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/*`
|
||||
|
||||
@@ -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<string, boolean> } = {};
|
||||
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: () => <div data-testid="body-dashboard" />,
|
||||
}));
|
||||
vi.mock("@/components/settings/insights-section", () => ({
|
||||
InsightsSection: () => <div data-testid="body-insights" />,
|
||||
}));
|
||||
vi.mock("@/components/settings/medications-section", () => ({
|
||||
MedicationsSection: () => <div data-testid="body-medications" />,
|
||||
}));
|
||||
vi.mock("@/components/settings/mood-section", () => ({
|
||||
MoodSection: () => <div data-testid="body-mood" />,
|
||||
}));
|
||||
vi.mock("@/components/settings/labs-section", () => ({
|
||||
LabsSection: () => <div data-testid="body-labs" />,
|
||||
}));
|
||||
vi.mock("@/components/settings/illness-section", () => ({
|
||||
IllnessSection: () => <div data-testid="body-illness" />,
|
||||
}));
|
||||
vi.mock("@/components/settings/vorsorge-section", () => ({
|
||||
VorsorgeSection: () => <div data-testid="body-vorsorge" />,
|
||||
}));
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { LayoutSection } from "../layout-section";
|
||||
|
||||
function render(locale: "en" | "de" = "en") {
|
||||
function render(modules?: Record<string, boolean>, locale: "en" | "de" = "en") {
|
||||
authRef.modules = modules;
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale={locale}>
|
||||
<LayoutSection />
|
||||
@@ -25,32 +60,57 @@ function render(locale: "en" | "de" = "en") {
|
||||
}
|
||||
|
||||
describe("<LayoutSection>", () => {
|
||||
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 `<a href="/settings/…">`.
|
||||
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.");
|
||||
|
||||
@@ -182,13 +182,18 @@ describe("settings sections — SSR smoke", () => {
|
||||
expect(html).toContain("settings-section-ai-title");
|
||||
});
|
||||
|
||||
it("<IntegrationsSection> renders Withings card title", () => {
|
||||
it("<IntegrationsSection> renders Withings card + the delivery-channels group", () => {
|
||||
const html = renderFramed("integrations", <IntegrationsSection />);
|
||||
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("<NotificationsSection> renders the reminders + delivery-channels groups", () => {
|
||||
it("<NotificationsSection> renders the reminders group only (channels moved out, v1.25.7)", () => {
|
||||
const html = renderFramed("notifications", <NotificationsSection />);
|
||||
// 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.");
|
||||
});
|
||||
|
||||
@@ -226,26 +226,26 @@ describe("<SettingsShell>", () => {
|
||||
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 =
|
||||
/<a\b[^>]*\baria-current="page"[^>]*\bhref="\/settings\/mood"|<a\b[^>]*\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("<SettingsShell>", () => {
|
||||
// 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("<SettingsShell>", () => {
|
||||
// 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
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
{/* v1.23 — active-session management (issue #64) + the security-activity
|
||||
feed. Both sit in the account/security block under the credential
|
||||
controls above. */}
|
||||
<SecuritySessionsCard isAuthenticated={isAuthenticated} />
|
||||
<SecurityActivityCard isAuthenticated={isAuthenticated} />
|
||||
{/* 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
|
||||
|
||||
@@ -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 `<SettingsSectionFrame>` in the route; the
|
||||
// body is the health-record export panel only.
|
||||
return <HealthRecordExportPanel />;
|
||||
const { t } = useTranslations();
|
||||
|
||||
// v1.18.6 (W9) — the visible page heading + subtitle come from the shared
|
||||
// `<SettingsSectionFrame>` in the route; the export panel is the primary
|
||||
// surface, with the share-links group sequenced below it.
|
||||
return (
|
||||
<div className="space-y-10">
|
||||
<HealthRecordExportPanel />
|
||||
|
||||
<section id="sharing" className="scroll-mt-28 space-y-4">
|
||||
<div className="space-y-0.5">
|
||||
<h2 className="text-foreground text-lg font-semibold">
|
||||
{t("settings.sections.sharing.title")}
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("settings.sections.sharing.subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
<SharingSection />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,19 +3,27 @@
|
||||
/**
|
||||
* `<IntegrationsSection>` — 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 `<SettingsSectionFrame>` in the route; the
|
||||
// body is the connections panel only.
|
||||
return <ConnectionsPanel />;
|
||||
const { t } = useTranslations();
|
||||
|
||||
// v1.18.6 (W9) — the visible page heading + subtitle come from the shared
|
||||
// `<SettingsSectionFrame>` in the route; the connections panel is the
|
||||
// primary surface, with the delivery-channels group sequenced below it.
|
||||
return (
|
||||
<div className="space-y-10">
|
||||
<ConnectionsPanel />
|
||||
|
||||
{/* 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. */}
|
||||
<section id="channels" className="scroll-mt-28 space-y-4">
|
||||
<div className="space-y-0.5">
|
||||
<h2 className="text-foreground text-lg font-semibold">
|
||||
{t("settings.sections.integrations.channelsHeading")}
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("settings.sections.integrations.channelsDescription")}
|
||||
</p>
|
||||
</div>
|
||||
<NotificationChannelsPanel />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 `<section>` 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>`). */
|
||||
id: string;
|
||||
/** i18n key under `settings.sections.layout.<slug>.title`. */
|
||||
titleKey: string;
|
||||
/** i18n key under `settings.sections.layout.<slug>.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<LayoutLink> = [
|
||||
const LAYOUT_GROUPS: ReadonlyArray<LayoutGroup> = [
|
||||
{
|
||||
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 (
|
||||
<ul className="space-y-3">
|
||||
{LAYOUT_LINKS.map((link) => {
|
||||
const Icon = link.icon;
|
||||
<div className="space-y-10">
|
||||
{visibleGroups.map((group) => {
|
||||
const { Body } = group;
|
||||
return (
|
||||
<li key={link.href}>
|
||||
<SettingsCard
|
||||
as={Link}
|
||||
href={link.href}
|
||||
className="hover:bg-accent/40 group flex items-center gap-4 transition-colors"
|
||||
>
|
||||
<Icon
|
||||
className="text-muted-foreground h-5 w-5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className="min-w-0 flex-1 space-y-0.5">
|
||||
<p className="text-sm font-semibold">{t(link.titleKey)}</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t(link.descriptionKey)}
|
||||
</p>
|
||||
</div>
|
||||
<ChevronRight
|
||||
className="text-muted-foreground/60 group-hover:text-foreground h-4 w-4 shrink-0 transition-colors"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</SettingsCard>
|
||||
</li>
|
||||
<section
|
||||
key={group.id}
|
||||
id={group.id}
|
||||
className="scroll-mt-28 space-y-4"
|
||||
>
|
||||
<div className="space-y-0.5">
|
||||
<h2 className="text-foreground text-lg font-semibold">
|
||||
{t(group.titleKey)}
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t(group.descriptionKey)}
|
||||
</p>
|
||||
</div>
|
||||
<Body />
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
/**
|
||||
* `<NotificationsSection>` — 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
|
||||
* `<NotificationChannelsPanel>`. 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
|
||||
* `<SettingsSectionFrame>`. The proactive-Coach nudge card lives under
|
||||
@@ -77,21 +71,6 @@ export function NotificationsSection() {
|
||||
) : null}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{/* Group 2 — Delivery channels ("where it's delivered"). The existing
|
||||
channels panel, unchanged. `id="channels"` anchors the group for the
|
||||
`/settings/channels` redirect. */}
|
||||
<section id="channels" className="scroll-mt-28 space-y-4">
|
||||
<div className="space-y-0.5">
|
||||
<h2 className="text-foreground text-lg font-semibold">
|
||||
{t("settings.sections.notifications.channelsHeading")}
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("settings.sections.notifications.channelsDescription")}
|
||||
</p>
|
||||
</div>
|
||||
<NotificationChannelsPanel />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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#<anchor>` (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<string> = 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#<slug>`,
|
||||
// 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. */
|
||||
|
||||
Reference in new issue
Block a user