From 001c158203fb6b322096752cf58fa1d0948b390d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?=
Date: Mon, 15 Jun 2026 00:59:33 +0200
Subject: [PATCH 1/2] feat(integrations): add a Setup-guide doc-link to every
integration card
Each integration card now carries the same discreet "Setup guide" link to
its runbook under docs.healthlog.dev/integrations/, so a user who
is mid-setup always knows where to go. The docs host lives in one shared
constant and the affordance renders identically across all six providers
(WHOOP, Withings, Fitbit, Polar, Oura, Nightscout).
---
messages/de.json | 33 ++++++++++-
messages/en.json | 33 ++++++++++-
messages/es.json | 33 ++++++++++-
messages/fr.json | 33 ++++++++++-
messages/it.json | 33 ++++++++++-
messages/pl.json | 33 ++++++++++-
.../__tests__/integrations-section.test.tsx | 39 +++++++++++++
.../settings/integrations/fitbit-card.tsx | 3 +
.../settings/integrations/nightscout-card.tsx | 3 +
.../integrations/oauth-provider-card.tsx | 3 +
.../integrations/setup-guide-link.tsx | 57 +++++++++++++++++++
.../settings/integrations/whoop-card.tsx | 3 +
.../settings/integrations/withings-card.tsx | 3 +
13 files changed, 303 insertions(+), 6 deletions(-)
create mode 100644 src/components/settings/integrations/setup-guide-link.tsx
diff --git a/messages/de.json b/messages/de.json
index bb1026aa7..3acdc22e7 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -4087,6 +4087,36 @@
"mood": {
"title": "Stimmungs-Tags",
"description": "Gruppen, Tags und Reihenfolge der Tag-Auswahl."
+ },
+ "reminders": {
+ "title": "Erinnerungen & Benachrichtigungen",
+ "description": "Ein Ort für jede Erinnerung — woran und wann erinnert wird — und die Kanäle, die sie zustellen.",
+ "categoriesGroup": "Erinnerungen",
+ "channelsGroup": "Zustellkanäle",
+ "medication": {
+ "title": "Medikamenten-Erinnerungen",
+ "description": "Einnahmezeiten und Zeitfenster, je Medikament eingestellt."
+ },
+ "vorsorge": {
+ "title": "Vorsorge",
+ "description": "Wiederkehrende Untersuchungen und Messungen, inklusive Ort."
+ },
+ "mood": {
+ "title": "Stimmungs-Check-in",
+ "description": "Eine tägliche Erinnerung, dein Befinden zu erfassen."
+ },
+ "lowStock": {
+ "title": "Reichweite bei Restbestand",
+ "description": "Werde gewarnt, bevor ein Medikament zur Neige geht."
+ },
+ "coach": {
+ "title": "Coach-Hinweis",
+ "description": "Proaktive Impulse vom KI-Coach."
+ },
+ "channels": {
+ "title": "Benachrichtigungskanäle",
+ "description": "APNs, Telegram, ntfy, Web Push, Webhook und E-Mail."
+ }
}
},
"profile": "Profil",
@@ -4773,7 +4803,8 @@
"token": "Der Token-Austausch mit Oura ist fehlgeschlagen. Bitte starte die Verbindung erneut.",
"rate_limited": "Zu viele Verbindungsversuche. Warte einen Moment und versuche es erneut.",
"generic": "Unbekannter Fehler bei der Verbindung. Bitte starte die Verbindung erneut."
- }
+ },
+ "integrationSetupGuide": "Einrichtungsanleitung"
},
"admin": {
"title": "Administration",
diff --git a/messages/en.json b/messages/en.json
index 63210fddf..8339967fc 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -4087,6 +4087,36 @@
"mood": {
"title": "Mood tags",
"description": "Groups, tags, and order of the mood picker."
+ },
+ "reminders": {
+ "title": "Reminders & Notifications",
+ "description": "One home for every reminder — what gets reminded and when — plus the channels that deliver them.",
+ "categoriesGroup": "Reminders",
+ "channelsGroup": "Delivery channels",
+ "medication": {
+ "title": "Medication reminders",
+ "description": "Dose times and windows, configured on each medication."
+ },
+ "vorsorge": {
+ "title": "Preventive care",
+ "description": "Recurring check-ups and measurements, with where to go."
+ },
+ "mood": {
+ "title": "Mood check-in",
+ "description": "A daily nudge to log how you feel."
+ },
+ "lowStock": {
+ "title": "Low-stock runway",
+ "description": "Get warned before a medication runs out."
+ },
+ "coach": {
+ "title": "Coach nudge",
+ "description": "Proactive prompts from the AI coach."
+ },
+ "channels": {
+ "title": "Notification channels",
+ "description": "APNs, Telegram, ntfy, Web Push, Webhook, and Email."
+ }
}
},
"profile": "Profile",
@@ -4773,7 +4803,8 @@
"token": "The token exchange with Oura failed. Please start the connection again.",
"rate_limited": "Too many connection attempts. Wait a moment and try again.",
"generic": "Unknown error while connecting. Please start the connection again."
- }
+ },
+ "integrationSetupGuide": "Setup guide"
},
"admin": {
"title": "Administration",
diff --git a/messages/es.json b/messages/es.json
index 2da94f8f0..f5c8d5fcb 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -4087,6 +4087,36 @@
"mood": {
"title": "Etiquetas de ánimo",
"description": "Grupos, etiquetas y orden del selector de ánimo."
+ },
+ "reminders": {
+ "title": "Recordatorios y notificaciones",
+ "description": "Un único lugar para cada recordatorio — qué se recuerda y cuándo — y los canales que lo entregan.",
+ "categoriesGroup": "Recordatorios",
+ "channelsGroup": "Canales de entrega",
+ "medication": {
+ "title": "Recordatorios de medicación",
+ "description": "Horas y ventanas de dosis, configuradas en cada medicamento."
+ },
+ "vorsorge": {
+ "title": "Cuidado preventivo",
+ "description": "Revisiones y mediciones periódicas, con el lugar al que acudir."
+ },
+ "mood": {
+ "title": "Registro de ánimo",
+ "description": "Un aviso diario para registrar cómo te sientes."
+ },
+ "lowStock": {
+ "title": "Margen por bajo stock",
+ "description": "Recibe un aviso antes de que se acabe un medicamento."
+ },
+ "coach": {
+ "title": "Sugerencia del coach",
+ "description": "Indicaciones proactivas del coach de IA."
+ },
+ "channels": {
+ "title": "Canales de notificación",
+ "description": "APNs, Telegram, ntfy, Web Push, Webhook y correo."
+ }
}
},
"profile": "Perfil",
@@ -4773,7 +4803,8 @@
"token": "El intercambio de token con Oura falló. Inicia la conexión de nuevo.",
"rate_limited": "Demasiados intentos de conexión. Espera un momento e inténtalo de nuevo.",
"generic": "Error desconocido al conectar. Inicia la conexión de nuevo."
- }
+ },
+ "integrationSetupGuide": "Guía de configuración"
},
"admin": {
"title": "Administration",
diff --git a/messages/fr.json b/messages/fr.json
index 21461fbc4..7506b034d 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -4087,6 +4087,36 @@
"mood": {
"title": "Étiquettes d’humeur",
"description": "Groupes, étiquettes et ordre du sélecteur d’humeur."
+ },
+ "reminders": {
+ "title": "Rappels et notifications",
+ "description": "Un seul endroit pour chaque rappel — quoi et quand — ainsi que les canaux qui les délivrent.",
+ "categoriesGroup": "Rappels",
+ "channelsGroup": "Canaux de diffusion",
+ "medication": {
+ "title": "Rappels de médication",
+ "description": "Heures et fenêtres de prise, configurées sur chaque médicament."
+ },
+ "vorsorge": {
+ "title": "Prévention",
+ "description": "Bilans et mesures récurrents, avec le lieu où se rendre."
+ },
+ "mood": {
+ "title": "Suivi de l'humeur",
+ "description": "Un rappel quotidien pour noter votre ressenti."
+ },
+ "lowStock": {
+ "title": "Marge de stock faible",
+ "description": "Soyez averti avant qu'un médicament ne s'épuise."
+ },
+ "coach": {
+ "title": "Suggestion du coach",
+ "description": "Invitations proactives du coach IA."
+ },
+ "channels": {
+ "title": "Canaux de notification",
+ "description": "APNs, Telegram, ntfy, Web Push, Webhook et e-mail."
+ }
}
},
"profile": "Profil",
@@ -4773,7 +4803,8 @@
"token": "L'échange de jeton avec Oura a échoué. Veuillez relancer la connexion.",
"rate_limited": "Trop de tentatives de connexion. Patientez un instant et réessayez.",
"generic": "Erreur inconnue lors de la connexion. Veuillez relancer la connexion."
- }
+ },
+ "integrationSetupGuide": "Guide de configuration"
},
"admin": {
"title": "Administration",
diff --git a/messages/it.json b/messages/it.json
index 265885255..0719bf886 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -4087,6 +4087,36 @@
"mood": {
"title": "Tag dell’umore",
"description": "Gruppi, tag e ordine del selettore dell’umore."
+ },
+ "reminders": {
+ "title": "Promemoria e notifiche",
+ "description": "Un'unica sede per ogni promemoria — cosa ricordare e quando — e i canali che li recapitano.",
+ "categoriesGroup": "Promemoria",
+ "channelsGroup": "Canali di consegna",
+ "medication": {
+ "title": "Promemoria dei farmaci",
+ "description": "Orari e finestre delle dosi, impostati su ogni farmaco."
+ },
+ "vorsorge": {
+ "title": "Prevenzione",
+ "description": "Controlli e misurazioni ricorrenti, con il luogo dove recarsi."
+ },
+ "mood": {
+ "title": "Check-in dell'umore",
+ "description": "Un promemoria quotidiano per registrare come ti senti."
+ },
+ "lowStock": {
+ "title": "Autonomia scorte",
+ "description": "Ricevi un avviso prima che un farmaco finisca."
+ },
+ "coach": {
+ "title": "Suggerimento del coach",
+ "description": "Stimoli proattivi dal coach IA."
+ },
+ "channels": {
+ "title": "Canali di notifica",
+ "description": "APNs, Telegram, ntfy, Web Push, Webhook ed e-mail."
+ }
}
},
"profile": "Profilo",
@@ -4773,7 +4803,8 @@
"token": "Lo scambio del token con Oura è fallito. Riavvia la connessione.",
"rate_limited": "Troppi tentativi di connessione. Attendi un momento e riprova.",
"generic": "Errore sconosciuto durante la connessione. Riavvia la connessione."
- }
+ },
+ "integrationSetupGuide": "Guida alla configurazione"
},
"admin": {
"title": "Administration",
diff --git a/messages/pl.json b/messages/pl.json
index a969c66f2..ae04caf95 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -4087,6 +4087,36 @@
"mood": {
"title": "Tagi nastroju",
"description": "Grupy, tagi i kolejność w wyborze nastroju."
+ },
+ "reminders": {
+ "title": "Przypomnienia i powiadomienia",
+ "description": "Jedno miejsce dla każdego przypomnienia — o czym i kiedy — oraz kanały, które je dostarczają.",
+ "categoriesGroup": "Przypomnienia",
+ "channelsGroup": "Kanały dostarczania",
+ "medication": {
+ "title": "Przypomnienia o lekach",
+ "description": "Godziny i okna dawek, ustawiane przy każdym leku."
+ },
+ "vorsorge": {
+ "title": "Profilaktyka",
+ "description": "Cykliczne badania i pomiary wraz z miejscem, gdzie się udać."
+ },
+ "mood": {
+ "title": "Zapis nastroju",
+ "description": "Codzienne przypomnienie, by zapisać samopoczucie."
+ },
+ "lowStock": {
+ "title": "Zapas na wyczerpaniu",
+ "description": "Otrzymaj ostrzeżenie, zanim lek się skończy."
+ },
+ "coach": {
+ "title": "Podpowiedź trenera",
+ "description": "Proaktywne wskazówki od trenera AI."
+ },
+ "channels": {
+ "title": "Kanały powiadomień",
+ "description": "APNs, Telegram, ntfy, Web Push, Webhook i e-mail."
+ }
}
},
"profile": "Profil",
@@ -4773,7 +4803,8 @@
"token": "Wymiana tokena z Oura nie powiodła się. Rozpocznij połączenie ponownie.",
"rate_limited": "Zbyt wiele prób połączenia. Poczekaj chwilę i spróbuj ponownie.",
"generic": "Nieznany błąd podczas łączenia. Rozpocznij połączenie ponownie."
- }
+ },
+ "integrationSetupGuide": "Przewodnik konfiguracji"
},
"admin": {
"title": "Administracja",
diff --git a/src/components/settings/__tests__/integrations-section.test.tsx b/src/components/settings/__tests__/integrations-section.test.tsx
index 8381da09e..69b49d1fb 100644
--- a/src/components/settings/__tests__/integrations-section.test.tsx
+++ b/src/components/settings/__tests__/integrations-section.test.tsx
@@ -303,4 +303,43 @@ describe("IntegrationsSection — single-status-display contract (A5)", () => {
// Fitbit / Google Health). The moodLog integration was removed.
expect(count(html, 'data-testid="integration-card-divider"')).toBe(3);
});
+
+ // v1.17.1 — every integration card carries the same discreet "Setup guide"
+ // doc-link pointing at its runbook under the single shared docs base. The
+ // affordance is one family across all six providers.
+ it("every integration card carries the shared Setup-guide doc-link", () => {
+ setIntegrationStatus({
+ threshold: 3,
+ integrations: [
+ {
+ integration: "withings",
+ state: "connected",
+ lastSuccessAt: "2026-05-09T18:00:00.000Z",
+ lastAttemptAt: "2026-05-09T18:00:00.000Z",
+ lastError: null,
+ connected: true,
+ configured: true,
+ legacyLastSyncedAt: "2026-05-09T18:00:00.000Z",
+ hasActivityScope: true,
+ },
+ ],
+ });
+
+ const html = render();
+ // One setup-guide link per card → six providers.
+ expect(count(html, 'data-slot="integration-setup-guide"')).toBe(6);
+ for (const provider of [
+ "withings",
+ "whoop",
+ "fitbit",
+ "polar",
+ "oura",
+ "nightscout",
+ ]) {
+ expect(html).toContain(`data-testid="${provider}-setup-guide"`);
+ expect(html).toContain(
+ `href="https://docs.healthlog.dev/integrations/${provider}"`,
+ );
+ }
+ });
});
diff --git a/src/components/settings/integrations/fitbit-card.tsx b/src/components/settings/integrations/fitbit-card.tsx
index 5b394c615..f2b1c3b04 100644
--- a/src/components/settings/integrations/fitbit-card.tsx
+++ b/src/components/settings/integrations/fitbit-card.tsx
@@ -51,6 +51,7 @@ import {
pillStateFor,
type IntegrationStatusViewModel,
} from "./shared";
+import { IntegrationSetupGuideLink } from "./setup-guide-link";
export function FitbitCard({
viewModel,
@@ -484,6 +485,8 @@ export function FitbitCard({
{t("settings.fitbitNoCredentials")}
)}
+
+
);
diff --git a/src/components/settings/integrations/nightscout-card.tsx b/src/components/settings/integrations/nightscout-card.tsx
index 97edbc3e7..6dc028bb9 100644
--- a/src/components/settings/integrations/nightscout-card.tsx
+++ b/src/components/settings/integrations/nightscout-card.tsx
@@ -39,6 +39,7 @@ import { useTranslations } from "@/lib/i18n/context";
import { invalidateKeys, measurementDependentKeys, queryKeys } from "@/lib/query-keys";
import { IntegrationErrorMessage } from "./shared";
+import { IntegrationSetupGuideLink } from "./setup-guide-link";
interface NightscoutStatus {
connected: boolean;
@@ -366,6 +367,8 @@ export function NightscoutCard({ enabled = true }: { enabled?: boolean }) {
>
)}
+
+
);
diff --git a/src/components/settings/integrations/oauth-provider-card.tsx b/src/components/settings/integrations/oauth-provider-card.tsx
index 77b2e97fd..d7642ecab 100644
--- a/src/components/settings/integrations/oauth-provider-card.tsx
+++ b/src/components/settings/integrations/oauth-provider-card.tsx
@@ -37,6 +37,7 @@ import { TestConnectionButton } from "@/components/settings/test-connection-butt
import { apiFetchRaw, apiGet, apiPost } from "@/lib/api/api-fetch";
import { useTranslations } from "@/lib/i18n/context";
import { invalidateKeys, measurementDependentKeys } from "@/lib/query-keys";
+import { IntegrationSetupGuideLink } from "./setup-guide-link";
export interface OAuthProviderStatus {
connected: boolean;
@@ -398,6 +399,8 @@ export function OAuthProviderCard({
{msg}
)}
+
+
);
diff --git a/src/components/settings/integrations/setup-guide-link.tsx b/src/components/settings/integrations/setup-guide-link.tsx
new file mode 100644
index 000000000..a6cf6a652
--- /dev/null
+++ b/src/components/settings/integrations/setup-guide-link.tsx
@@ -0,0 +1,57 @@
+"use client";
+
+/**
+ * v1.17.1 — shared "Setup guide" doc-link for every Settings → Integrations
+ * card. Each card paints the same affordance: one discreet, external link to
+ * the provider's setup runbook ("was eingeben, wo klicken"). Single-sourced
+ * here so all six cards (WHOOP / Withings / Fitbit / Polar / Oura / Nightscout)
+ * read as one family and the docs host lives in exactly one place.
+ *
+ * The runbooks themselves are authored separately; this link wires the
+ * destination now so a user who is mid-setup always knows where to go.
+ */
+
+import { ExternalLink } from "lucide-react";
+
+import { useTranslations } from "@/lib/i18n/context";
+
+/**
+ * Base for every provider setup runbook. The provider key is appended as a
+ * path segment, e.g. `https://docs.healthlog.dev/integrations/whoop`. Kept as
+ * a single constant so the host never drifts across cards.
+ */
+export const INTEGRATION_DOCS_BASE =
+ "https://docs.healthlog.dev/integrations";
+
+export type IntegrationDocsProvider =
+ | "whoop"
+ | "withings"
+ | "fitbit"
+ | "polar"
+ | "oura"
+ | "nightscout";
+
+export function integrationDocsHref(provider: IntegrationDocsProvider): string {
+ return `${INTEGRATION_DOCS_BASE}/${provider}`;
+}
+
+export function IntegrationSetupGuideLink({
+ provider,
+}: {
+ provider: IntegrationDocsProvider;
+}) {
+ const { t } = useTranslations();
+ return (
+
+ {t("settings.integrationSetupGuide")}
+
+
+ );
+}
diff --git a/src/components/settings/integrations/whoop-card.tsx b/src/components/settings/integrations/whoop-card.tsx
index a3acc2077..23b875f4e 100644
--- a/src/components/settings/integrations/whoop-card.tsx
+++ b/src/components/settings/integrations/whoop-card.tsx
@@ -43,6 +43,7 @@ import {
pillStateFor,
type IntegrationStatusViewModel,
} from "./shared";
+import { IntegrationSetupGuideLink } from "./setup-guide-link";
export function WhoopCard({
viewModel,
@@ -456,6 +457,8 @@ export function WhoopCard({
{t("settings.whoopNoCredentials")}
)}
+
+
);
diff --git a/src/components/settings/integrations/withings-card.tsx b/src/components/settings/integrations/withings-card.tsx
index d9613697a..009566d7d 100644
--- a/src/components/settings/integrations/withings-card.tsx
+++ b/src/components/settings/integrations/withings-card.tsx
@@ -36,6 +36,7 @@ import {
pillStateFor,
type IntegrationStatusViewModel,
} from "./shared";
+import { IntegrationSetupGuideLink } from "./setup-guide-link";
export function WithingsCard({
viewModel,
@@ -491,6 +492,8 @@ export function WithingsCard({
{t("settings.withingsNoCredentials")}
)}
+
+
);
From 78c110a6c868c74a8865915bf0a6db8020e53d42 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?=
Date: Mon, 15 Jun 2026 00:59:47 +0200
Subject: [PATCH 2/2] feat(settings): add a Reminders & Notifications home
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Reminders used to be scattered across three places: medication reminders on
each med card, mood / low-stock / coach-nudge inside the Notifications channel
screen, and the Vorsorge reminders on their own page. The new Reminders
section is the single front door. It draws a clean line between the two
concepts — reminder categories (the what/when) and notification channels (the
how/where) — and links to each canonical editor, which keeps its own route and
deep link intact. Consolidation by linking, not a rewrite.
The section sits right after Notifications in the settings nav.
---
src/app/settings/[section]/page.tsx | 2 +
.../__tests__/reminders-section.test.tsx | 67 +++++++
.../__tests__/settings-shell.test.tsx | 3 +
src/components/settings/reminders-section.tsx | 176 ++++++++++++++++++
.../settings/section-placeholder.tsx | 2 +
src/components/settings/section-slugs.ts | 6 +
src/components/settings/settings-shell.tsx | 10 +
7 files changed, 266 insertions(+)
create mode 100644 src/components/settings/__tests__/reminders-section.test.tsx
create mode 100644 src/components/settings/reminders-section.tsx
diff --git a/src/app/settings/[section]/page.tsx b/src/app/settings/[section]/page.tsx
index 20f1d2d25..87b97f90f 100644
--- a/src/app/settings/[section]/page.tsx
+++ b/src/app/settings/[section]/page.tsx
@@ -14,6 +14,7 @@ import { LayoutSection } from "@/components/settings/layout-section";
import { MedicationsSection } from "@/components/settings/medications-section";
import { MoodSection } from "@/components/settings/mood-section";
import { NotificationsSection } from "@/components/settings/notifications-section";
+import { RemindersSection } from "@/components/settings/reminders-section";
import { SectionPlaceholder } from "@/components/settings/section-placeholder";
// v1.8.7.1 — `thresholds` (Targets) and `sources` (Sources) are two
// separate sections again. `ThresholdsSection` renders the target-range
@@ -52,6 +53,7 @@ const SECTION_COMPONENTS: Record<
ai: AiSection,
integrations: IntegrationsSection,
notifications: NotificationsSection,
+ reminders: RemindersSection,
layout: LayoutSection,
dashboard: DashboardSection,
insights: InsightsSection,
diff --git a/src/components/settings/__tests__/reminders-section.test.tsx b/src/components/settings/__tests__/reminders-section.test.tsx
new file mode 100644
index 000000000..3305f95d1
--- /dev/null
+++ b/src/components/settings/__tests__/reminders-section.test.tsx
@@ -0,0 +1,67 @@
+/**
+ * v1.17.1 — the "Reminders & Notifications" hub.
+ *
+ * The hub is the single front door for every reminder concept. It groups the
+ * reminder CATEGORIES (medication / Vorsorge / mood / low-stock / coach) and
+ * links to the notification CHANNELS, with each link deep-linking into the
+ * canonical editor so nothing is rewritten — only gathered in one place.
+ */
+import { describe, expect, it } from "vitest";
+import { renderToStaticMarkup } from "react-dom/server";
+
+import { I18nProvider } from "@/lib/i18n/context";
+import { RemindersSection } from "../reminders-section";
+
+function render(locale: "en" | "de" = "en") {
+ return renderToStaticMarkup(
+
+
+ ,
+ );
+}
+
+describe("", () => {
+ it("deep-links to every canonical reminder editor", () => {
+ const html = render();
+ for (const href of [
+ "/medications",
+ "/vorsorge",
+ "/settings/notifications#mood-reminder",
+ "/settings/notifications#low-stock",
+ "/settings/notifications#coach-nudge",
+ ]) {
+ expect(html).toContain(`href="${href}"`);
+ }
+ });
+
+ it("links to the notification channels screen (the how/where)", () => {
+ const html = render();
+ expect(html).toContain('href="/settings/notifications"');
+ expect(html).toContain('data-testid="reminders-link-channels"');
+ });
+
+ it("renders a card for each reminder category", () => {
+ const html = render();
+ for (const testId of [
+ "reminders-link-medication",
+ "reminders-link-vorsorge",
+ "reminders-link-mood",
+ "reminders-link-low-stock",
+ "reminders-link-coach",
+ ]) {
+ expect(html).toContain(`data-testid="${testId}"`);
+ }
+ });
+
+ it("separates categories (what/when) from channels (how/where)", () => {
+ const html = render();
+ expect(html).toContain("Reminders");
+ expect(html).toContain("Delivery channels");
+ });
+
+ it("resolves its copy in German too", () => {
+ const html = render("de");
+ expect(html).toContain("Medikamenten-Erinnerungen");
+ expect(html).toContain("Zustellkanäle");
+ });
+});
diff --git a/src/components/settings/__tests__/settings-shell.test.tsx b/src/components/settings/__tests__/settings-shell.test.tsx
index 361c4a524..b10b03c07 100644
--- a/src/components/settings/__tests__/settings-shell.test.tsx
+++ b/src/components/settings/__tests__/settings-shell.test.tsx
@@ -61,10 +61,13 @@ describe("SETTINGS_SECTION_SLUGS", () => {
// slugs (`dashboard`, `insights`, `medications`, `mood`) keep their
// routes so deep links resolve, but they are reached through the
// Layout hub instead of four standalone nav entries.
+ // v1.17.1 — `reminders` is the one "Reminders & Notifications" home and
+ // sits right after `notifications`.
expect([...SETTINGS_SECTION_SLUGS]).toEqual([
"account",
"integrations",
"notifications",
+ "reminders",
"layout",
"dashboard",
"insights",
diff --git a/src/components/settings/reminders-section.tsx b/src/components/settings/reminders-section.tsx
new file mode 100644
index 000000000..698465751
--- /dev/null
+++ b/src/components/settings/reminders-section.tsx
@@ -0,0 +1,176 @@
+"use client";
+
+import Link from "next/link";
+import {
+ Bell,
+ ChevronRight,
+ HeartPulse,
+ MessageCircle,
+ Package,
+ Pill,
+ Smile,
+ Stethoscope,
+ type LucideIcon,
+} from "lucide-react";
+
+import { cn } from "@/lib/utils";
+import { useTranslations } from "@/lib/i18n/context";
+
+/**
+ * v1.17.1 — the one "Reminders & Notifications" home.
+ *
+ * Before this section, "what gets reminded and when" was scattered across
+ * three unrelated homes: medication reminders lived per-medication on the med
+ * cards, mood + low-stock + coach-nudge lived inside the Notifications channel
+ * screen, and the new Vorsorge (preventive-care) reminders had their own
+ * page. There was no single place that answered "was erinnert mich woran,
+ * wann, und worüber".
+ *
+ * This hub is the single front door. It draws a clean line between two
+ * genuinely different concepts (so the maintainer rule "never split the same
+ * concept across a page" still holds):
+ *
+ * - CHANNELS — the *how/where* a reminder is delivered (APNs / Telegram /
+ * ntfy / Web Push / Webhook / Email). These keep living on the
+ * Notifications channel screen; this hub links to them.
+ * - CATEGORIES — the *what/when*: medication, mood, Vorsorge, low-stock
+ * runway, and the proactive Coach nudge. Each keeps its own canonical
+ * editor and deep link; the hub just gathers them so the concept reads
+ * as one place.
+ *
+ * Consolidation by linking — every existing editor stays intact and
+ * deep-linkable; nothing here is a rewrite.
+ */
+interface ReminderLink {
+ href: string;
+ icon: LucideIcon;
+ titleKey: string;
+ descriptionKey: string;
+ /** External-to-settings link (opens a feature page, not a settings tab). */
+ testId: string;
+}
+
+const CHANNEL_LINKS: ReadonlyArray = [
+ {
+ href: "/settings/notifications",
+ icon: Bell,
+ titleKey: "settings.sections.reminders.channels.title",
+ descriptionKey: "settings.sections.reminders.channels.description",
+ testId: "reminders-link-channels",
+ },
+];
+
+const CATEGORY_LINKS: ReadonlyArray = [
+ {
+ href: "/medications",
+ icon: Pill,
+ titleKey: "settings.sections.reminders.medication.title",
+ descriptionKey: "settings.sections.reminders.medication.description",
+ testId: "reminders-link-medication",
+ },
+ {
+ href: "/vorsorge",
+ icon: Stethoscope,
+ titleKey: "settings.sections.reminders.vorsorge.title",
+ descriptionKey: "settings.sections.reminders.vorsorge.description",
+ testId: "reminders-link-vorsorge",
+ },
+ {
+ href: "/settings/notifications#mood-reminder",
+ icon: Smile,
+ titleKey: "settings.sections.reminders.mood.title",
+ descriptionKey: "settings.sections.reminders.mood.description",
+ testId: "reminders-link-mood",
+ },
+ {
+ href: "/settings/notifications#low-stock",
+ icon: Package,
+ titleKey: "settings.sections.reminders.lowStock.title",
+ descriptionKey: "settings.sections.reminders.lowStock.description",
+ testId: "reminders-link-low-stock",
+ },
+ {
+ href: "/settings/notifications#coach-nudge",
+ icon: MessageCircle,
+ titleKey: "settings.sections.reminders.coach.title",
+ descriptionKey: "settings.sections.reminders.coach.description",
+ testId: "reminders-link-coach",
+ },
+];
+
+function ReminderLinkCard({ link }: { link: ReminderLink }) {
+ const { t } = useTranslations();
+ const Icon = link.icon;
+ return (
+
+
+ );
+}
diff --git a/src/components/settings/section-placeholder.tsx b/src/components/settings/section-placeholder.tsx
index 554c26cee..e437f3b95 100644
--- a/src/components/settings/section-placeholder.tsx
+++ b/src/components/settings/section-placeholder.tsx
@@ -12,6 +12,7 @@
*/
import {
+ AlarmClock,
Bell,
Download,
Info,
@@ -37,6 +38,7 @@ const SLUG_ICON: Record = {
account: User,
integrations: Link2,
notifications: Bell,
+ reminders: AlarmClock,
layout: LayoutDashboard,
dashboard: LayoutDashboard,
insights: TrendingUp,
diff --git a/src/components/settings/section-slugs.ts b/src/components/settings/section-slugs.ts
index fd886ef78..87e9723b3 100644
--- a/src/components/settings/section-slugs.ts
+++ b/src/components/settings/section-slugs.ts
@@ -27,10 +27,16 @@
// `layout` entry instead of four scattered "arrange" entries, so the
// concept reads as one place. The four editor slugs stay in this list so
// their routes still resolve; they are simply not listed in the shell nav.
+// v1.17.1 — `reminders` sits right after `notifications`: the one
+// "Reminders & Notifications" home that gathers the scattered reminder
+// categories (medication, mood, Vorsorge, low-stock, coach nudge) and links
+// to the notification channels. The canonical editors keep their own routes;
+// the hub is consolidation by linking.
export const SETTINGS_SECTION_SLUGS = [
"account",
"integrations",
"notifications",
+ "reminders",
"layout",
"dashboard",
"insights",
diff --git a/src/components/settings/settings-shell.tsx b/src/components/settings/settings-shell.tsx
index 530c48963..28d2c168d 100644
--- a/src/components/settings/settings-shell.tsx
+++ b/src/components/settings/settings-shell.tsx
@@ -18,6 +18,7 @@ import * as React from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import {
+ AlarmClock,
Bell,
Download,
Info,
@@ -89,6 +90,15 @@ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [
titleKey: "settings.sections.notifications.title",
icon: Bell,
},
+ // v1.17.1 — the one "Reminders & Notifications" home. Gathers the
+ // scattered reminder categories (medication / mood / Vorsorge / low-stock
+ // / coach nudge) and links to the notification channels. The canonical
+ // editors keep their own routes; this nav entry is the single front door.
+ {
+ slug: "reminders",
+ titleKey: "settings.sections.reminders.title",
+ icon: AlarmClock,
+ },
// v1.17.1 (F-2) — one "Layout & Personalization" nav entry replaces the
// four scattered Dashboard / Insights / Medications / Mood "arrange"
// entries. Those routes still resolve (deep links, page-header cogs, and