Merge v1.25.3: channels into notifications + Darstellung appearance hub

This commit is contained in:
Marc-André Bombeck committed 2026-06-29 15:49:39 +02:00
commit 7eca388f56
18 files changed
+347 -188

No files matched your search

@@ -63,7 +63,10 @@ export function VorsorgeSettings() {
// `<section>` blocks with `text-sm` headings.
return (
<div className="space-y-6">
<SettingsCard>
{/* v1.25.3 — `id="vorsorge-view"` + `scroll-mt-28` so the Appearance hub
deep-link (`/settings/vorsorge#vorsorge-view`) lands on this card
below the sticky page header, mirroring the labs/illness anchors. */}
<SettingsCard id="vorsorge-view" className="scroll-mt-28">
<SettingsCardHeader
icon={LayoutGrid}
title={t("moduleList.viewHeading")}
@@ -1,9 +1,14 @@
/**
* v1.17.1 (F-2) — the "Layout & Personalization" hub.
* v1.17.1 (F-2) — the "Appearance" hub (slug stays `layout`).
*
* The hub is the single front door for the four personalization editors.
* It must link to each of them with consistent framing so the concept
* reads as one home rather than four scattered settings sections.
* 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.
*/
import { describe, expect, it } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
@@ -20,26 +25,34 @@ function render(locale: "en" | "de" = "en") {
}
describe("<LayoutSection>", () => {
it("links to the arrangement editors it still hosts", () => {
it("links to every view surface it indexes (anchor deep-links)", () => {
const html = render();
for (const href of ["/settings/dashboard", "/settings/insights"]) {
for (const href of [
"/settings/dashboard",
"/settings/insights",
"/settings/medications#medications-view",
"/settings/labs#labs-view",
"/settings/illness#illness-view",
"/settings/vorsorge#vorsorge-view",
]) {
expect(html).toContain(`href="${href}"`);
}
// v1.18.0 (S5) — Medications (Medikamente) and Mood (Stimmung)
// graduated to their own nav entries and are no longer linked here.
expect(html).not.toContain('href="/settings/medications"');
expect(html).not.toContain('href="/settings/mood"');
});
it("renders the hub heading and a card per editor", () => {
it("renders one card per indexed surface (6 links)", () => {
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");
});
it("resolves its copy in German too", () => {
const html = render("de");
expect(html).toContain("Layout");
// The hub body is the link list; the German module labels resolve.
expect(html).toContain("Medikamente");
expect(html).toContain("Labor");
expect(html).not.toContain("settings.sections.layout.");
});
});
@@ -188,19 +188,25 @@ describe("settings sections — SSR smoke", () => {
expect(html).toContain("Withings");
});
it("<NotificationsSection> renders the single reminder-types home", () => {
it("<NotificationsSection> renders the reminders + delivery-channels groups", () => {
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.
expect(html).toContain("settings-section-notifications-title");
expect(html).not.toContain("Notification channels");
expect(html).not.toContain('data-slot="notifications-channels-cross-link"');
expect(html).not.toContain('data-slot="notifications-inbox-cross-link"');
// The mood card fails OPEN (mock user carries no module map), so both
// remaining reminder-type cards render. The coach-nudge card is gone.
// reminder-type cards render under the Reminders group. The coach-nudge
// card is gone.
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"');
expect(html).toContain("Reminders");
expect(html).toContain("Delivery channels");
// No raw i18n key leaks past the provider.
expect(html).not.toContain("settings.sections.notifications.");
});
@@ -91,6 +91,9 @@ describe("SETTINGS_SECTION_SLUGS", () => {
// sits right after `ai`.
// v1.18.1 (D4) — `channels` + `sources` split out of the Integrations
// sub-tabs into their own left-side entries, right after `integrations`.
// v1.25.3 — `channels` folded back into Notifications as an in-page group;
// `/settings/channels` 301-redirects to `/settings/notifications#channels`.
// `sources` keeps its own entry.
// v1.18.7 — `labs`, `illness`, `vorsorge` move into the shell as
// first-class sections, right after `mood` (the three were standalone
// `ModuleSettingsFrame` pages).
@@ -102,7 +105,6 @@ describe("SETTINGS_SECTION_SLUGS", () => {
"security",
"modules",
"integrations",
"channels",
"sources",
"notifications",
"layout",
@@ -293,14 +295,15 @@ describe("<SettingsShell>", () => {
// (single-line; the longer "Notification channels" wrapped). The
// `/notifications` inbox now shares the "Notifications" label.
expect(html).toContain("Notifications");
// v1.17.1 (F-2) — the dashboard + insights arrangement editors are
// reached through one hub entry (route stays `/settings/layout`).
// v1.19.1 (S3) — the nav label now reads "Dashboard" to match where
// it leads; the route is unchanged.
// v1.17.1 (F-2) — the view/arrangement editors are reached through one hub
// entry (route stays `/settings/layout`). v1.25.3 — the hub is renamed
// "Appearance" (DE "Darstellung") and indexes every module's view surface;
// the route is unchanged.
expect(html).toContain('href="/settings/layout"');
expect(html).toContain(">Dashboard</a>");
// v1.18.1 (D4) — Channels + Sources are their own left-side entries.
expect(html).toContain('href="/settings/channels"');
expect(html).toContain(">Appearance</a>");
// v1.25.3 — Channels folded into Notifications, so it is no longer a
// 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.
@@ -334,15 +337,14 @@ describe("<SettingsShell>", () => {
// compound "Benachrichtigungs-Kanäle" wrapped). The `/notifications`
// inbox now shares the "Benachrichtigungen" label.
expect(html).toContain("Benachrichtigungen");
// v1.17.1 (F-2) — the arrangement editors are reached through one hub
// entry (route stays `/settings/layout`). v1.19.1 (S3) — the nav label
// reads "Dashboard" in every locale to match where it leads.
// v1.17.1 (F-2) — the view editors are reached through one hub entry
// (route stays `/settings/layout`). v1.25.3 — the German nav label reads
// "Darstellung" (Appearance) to match the renamed hub.
expect(html).toContain('href="/settings/layout"');
expect(html).toContain(">Dashboard</a>");
// v1.18.1 (D4) — Channels ("Kanäle") + Sources ("Quellen-Priorität")
// are their own left-side entries.
expect(html).toContain('href="/settings/channels"');
expect(html).toContain("Kanäle");
expect(html).toContain(">Darstellung</a>");
// 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"');
@@ -1,20 +0,0 @@
"use client";
/**
* `<ChannelsSection>` — Settings → Kanäle (notification delivery channels).
*
* v1.18.1 (D4) — split out of the Integrations sub-tabs into its own left-side
* entry. A channel (Telegram / ntfy / Web Push / Webhook / Email) is a
* delivery provider — the same family as a connected device — and now reads as
* a first-class settings home rather than a tab hidden behind Integrationen.
*
* The content is the existing `<NotificationChannelsPanel>`. v1.18.6 (W9) —
* the visible heading + subtitle now come from the shared
* `<SettingsSectionFrame>` in the route; this wrapper is the panel only.
*/
import { NotificationChannelsPanel } from "@/components/settings/integrations/notification-channels-panel";
export function ChannelsSection() {
return <NotificationChannelsPanel />;
}
+47 -13
View File
@@ -2,8 +2,12 @@
import Link from "next/link";
import {
CalendarCheck,
ChevronRight,
FlaskConical,
LayoutDashboard,
Pill,
Thermometer,
TrendingUp,
type LucideIcon,
} from "lucide-react";
@@ -12,17 +16,23 @@ import { useTranslations } from "@/lib/i18n/context";
import { SettingsCard } from "./settings-card";
/**
* v1.17.1 (F-2) — the one "Layout & Personalization" home.
* v1.17.1 (F-2) — the "Appearance" home (slug stays `layout`).
*
* Before this section, "how my app is laid out" was scattered across
* several unrelated settings sections with no shared framing. This
* section is the single front door for the arrangement editors: it
* presents them as one consistent set of cards and links to each editor,
* which keep their own routes and mutation flows intact.
* "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.
*
* v1.18.0 (S5) — Medications (Medikamente) and Mood (Stimmung) graduated
* to their own top-level nav entries, so the hub now hosts only the
* dashboard + insights arrangement editors.
* 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.
*/
interface LayoutLink {
href: string;
@@ -44,10 +54,34 @@ const LAYOUT_LINKS: ReadonlyArray<LayoutLink> = [
titleKey: "settings.sections.layout.insights.title",
descriptionKey: "settings.sections.layout.insights.description",
},
// v1.18.0 (S5) — Medications (Medikamente) and Mood (Stimmung) graduated
// to their own top-level nav entries, so they are no longer linked from
// the Layout hub. Only the dashboard + insights arrangement editors
// remain here.
// 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,
titleKey: "settings.sections.layout.medications.title",
descriptionKey: "settings.sections.layout.medications.description",
},
{
href: "/settings/labs#labs-view",
icon: FlaskConical,
titleKey: "settings.sections.layout.labs.title",
descriptionKey: "settings.sections.layout.labs.description",
},
{
href: "/settings/illness#illness-view",
icon: Thermometer,
titleKey: "settings.sections.layout.illness.title",
descriptionKey: "settings.sections.layout.illness.description",
},
{
href: "/settings/vorsorge#vorsorge-view",
icon: CalendarCheck,
titleKey: "settings.sections.layout.vorsorge.title",
descriptionKey: "settings.sections.layout.vorsorge.description",
},
];
export function LayoutSection() {
@@ -1,8 +1,10 @@
"use client";
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").
@@ -19,41 +21,77 @@ import { MoodReminderCard } from "@/components/settings/mood-reminder-card";
* (a toggleable fail-open module since D3, so a missing key reads as enabled
* and a stale `/me` payload never blanks a card).
*
* v1.18.1 (D5) — the page is intentionally lean: the section blurb, the
* channels / inbox cross-links, and the embedded Vorsorge (preventive-care)
* editor were all removed. Vorsorge has its own `/vorsorge` page, so embedding
* it here was a duplicate. Delivery CHANNELS (Telegram / ntfy / Web Push /
* Webhook / Email) live under Settings → Integrations → Channels.
* 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.18.6 (W9) — the visible heading + subtitle now come from the shared
* `<SettingsSectionFrame>`. The proactive-Coach nudge card moved to Settings →
* Coach (it is a Coach setting, not a generic notification), so this screen
* carries the mood + low-stock reminder cards only.
* v1.18.6 (W9) — the page heading + subtitle come from the shared
* `<SettingsSectionFrame>`. The proactive-Coach nudge card lives under
* Settings → Coach (it is a Coach setting, not a generic notification).
*/
export function NotificationsSection() {
const { isAuthenticated, user } = useAuth();
const { t } = useTranslations();
// v1.18.0 (S4) — module-gated per-type visibility. Fail OPEN (`!== false`)
// so a stale `/me` payload without the module map keeps every card visible.
const moodEnabled = user?.modules?.mood !== false;
const medsEnabled = user?.modules?.medications !== false;
const anyReminder = moodEnabled || medsEnabled;
return (
<div className="space-y-6">
{/* Mood check-in reminder — only when the mood module is enabled. */}
{moodEnabled ? (
<div id="mood-reminder" className="scroll-mt-28">
<MoodReminderCard isAuthenticated={isAuthenticated} />
</div>
<div className="space-y-10">
{/* Group 1 — Reminders ("what you receive"). Rendered only when at
least one reminder-type card is visible, so a fully-disabled
account never shows an empty heading. */}
{anyReminder ? (
<section className="space-y-4">
<div className="space-y-0.5">
<h2 className="text-foreground text-lg font-semibold">
{t("settings.sections.notifications.remindersHeading")}
</h2>
<p className="text-muted-foreground text-sm">
{t("settings.sections.notifications.remindersDescription")}
</p>
</div>
{/* Mood check-in reminder — only when the mood module is enabled. */}
{moodEnabled ? (
<div id="mood-reminder" className="scroll-mt-28">
<MoodReminderCard isAuthenticated={isAuthenticated} />
</div>
) : null}
{/* Medication low-stock runway — only when the medications module
is enabled (toggleable, fail-open, since D3). */}
{medsEnabled ? (
<div id="low-stock" className="scroll-mt-28">
<LowStockCard isAuthenticated={isAuthenticated} />
</div>
) : null}
</section>
) : null}
{/* Medication low-stock runway — only when the medications module is
enabled (toggleable, fail-open, since D3). */}
{medsEnabled ? (
<div id="low-stock" className="scroll-mt-28">
<LowStockCard isAuthenticated={isAuthenticated} />
{/* 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>
) : null}
<NotificationChannelsPanel />
</section>
</div>
);
}
@@ -29,7 +29,6 @@ import {
Link2,
Lock,
Pill,
Radio,
Settings2,
Share2,
ShieldCheck,
@@ -51,7 +50,6 @@ const SLUG_ICON: Record<SettingsSectionSlug, LucideIcon> = {
security: ShieldCheck,
modules: Blocks,
integrations: Link2,
channels: Radio,
sources: Layers,
notifications: Bell,
layout: LayoutDashboard,
+4 -3
View File
@@ -57,9 +57,10 @@ export const SETTINGS_SECTION_SLUGS = [
// single front door for enabling/disabling secondary tracking domains.
"modules",
"integrations",
// v1.18.1 (D4) — Kanäle (delivery channels) + Quellen (source weighting)
// split out of the Integrations tabs into their own left-side entries.
"channels",
// v1.18.1 (D4) — Quellen (source weighting) split out of the Integrations
// tabs into its own left-side entry. v1.25.3 — Kanäle (delivery channels)
// folded back into Notifications as an in-page group; `/settings/channels`
// 301-redirects to `/settings/notifications#channels` (next.config.ts).
"sources",
"notifications",
"layout",
+12 -15
View File
@@ -30,12 +30,11 @@ import {
Info,
KeyRound,
Plug,
LayoutDashboard,
Layers,
Link2,
Lock,
Palette,
Pill,
Radio,
Settings2,
Share2,
ShieldCheck,
@@ -123,13 +122,9 @@ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [
titleKey: "settings.sections.integrations.title",
icon: Link2,
},
// v1.18.1 (D4) — Kanäle (delivery channels) split out of the Integrations
// sub-tabs into its own entry. A channel is a delivery provider.
{
slug: "channels",
titleKey: "settings.sections.channels.title",
icon: Radio,
},
// v1.25.3 — the standalone Kanäle (delivery channels) entry folded back
// into Notifications as a labelled in-page group; `/settings/channels`
// 301-redirects to `/settings/notifications#channels`.
// v1.18.1 (D4) — Quellen (source weighting) split out into its own entry:
// which connection wins when two report the same metric.
{
@@ -145,15 +140,17 @@ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [
titleKey: "settings.sections.notifications.title",
icon: Bell,
},
// 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
// the hub's own links all work); they are simply reached through the
// Layout hub now, so "how my app is laid out" reads as one home.
// v1.17.1 (F-2) — one nav entry replaces the four scattered Dashboard /
// Insights / Medications / Mood "arrange" entries. Those routes still
// resolve (deep links, page-header cogs, and the hub's own links all work);
// they are reached through this hub now, so "how my app looks" reads as one
// 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.
{
slug: "layout",
titleKey: "settings.sections.layout.title",
icon: LayoutDashboard,
icon: Palette,
},
// v1.18.0 (S5) — Medikamente: medication-specific settings (list view +
// order + injection sites). Was a hidden child of the Layout hub.