refactor(settings): fold Sources into Integrationen as a sub-tab

Source priority decides which connection wins when two report the same
metric, so it belongs with the integrations rather than as a standalone
settings page. Sources renders as the third Integrations sub-tab
(Connections / Channels / Sources); the standalone `sources` slug is
removed from the shell nav, slug registry, and route dispatch, and
`/settings/sources` 301-redirects to `/settings/integrations`.
This commit is contained in:
Marc-André Bombeck committed 2026-06-15 22:58:44 +02:00
1 parent d3d8731209
commit 7b80a38ea2
8 files changed
+92 -66

No files matched your search

+9
View File
@@ -121,6 +121,15 @@ const nextConfig: NextConfig = {
destination: "/coach",
permanent: true,
},
// v1.18.0 (S3) — Sources folded into Settings → Integrations as the
// "Sources" sub-tab; the standalone `/settings/sources` page is gone.
// 301-redirect so bookmarks, the PWA's cached navigation, and the old
// cross-links keep resolving.
{
source: "/settings/sources",
destination: "/settings/integrations",
permanent: true,
},
];
},
async headers() {
+4 -7
View File
@@ -17,12 +17,10 @@ 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
// editor; `SourcesSection` (standalone mode) renders the source-priority
// ladders. Both are served by this dynamic route.
// v1.18.0 (S3) — `sources` no longer has a standalone route; source priority
// renders inside Settings → Integrations as the "Sources" sub-tab.
// `/settings/sources` 301-redirects to `/settings/integrations` (next.config.ts).
import { ThresholdsSection } from "@/components/settings/thresholds-section";
import { SourcesSection } from "@/components/settings/sources-section";
import {
SETTINGS_SECTION_SLUGS,
isSettingsSectionSlug,
@@ -32,7 +30,7 @@ import { SettingsShell } from "@/components/settings/settings-shell";
/**
* Dynamic settings section route. Each of the `SETTINGS_SECTION_SLUGS`
* (16 today — 12 nav-visible sections plus the four routable-but-hidden
* (15 today — 11 nav-visible sections plus the four routable-but-hidden
* Layout child editors) is pre-rendered at build via
* `generateStaticParams()` so the URLs are
* statically known to Next.js, while the `dynamicParams = false` flag below
@@ -64,7 +62,6 @@ const SECTION_COMPONENTS: Record<
medications: MedicationsSection,
mood: MoodSection,
thresholds: ThresholdsSection,
sources: () => <SourcesSection />,
api: ApiSection,
export: ExportSection,
advanced: AdvancedSection,
@@ -45,9 +45,10 @@ describe("SETTINGS_SECTION_SLUGS", () => {
// `api` and `advanced` so every "give me my data out" path lives in
// one place.
// v1.4.25 W5e added `sources` between `thresholds` and `ai`; v1.4.34
// IW-D merged it into `thresholds`.
// v1.8.7.1 — `sources` (Sources) is its own slug again, sitting
// between `thresholds` (Targets) and `ai`.
// IW-D merged it into `thresholds`; v1.8.7.1 split it back out.
// v1.18.0 (S3) — `sources` is no longer a standalone slug: source
// priority folded into Settings → Integrations as the "Sources"
// sub-tab. `/settings/sources` 301-redirects to `/settings/integrations`.
// v1.15.18 — `insights` sits between `dashboard` and `thresholds`: the
// overview-arrange + pill-sort customise surface for `/insights`.
// v1.16.10 — `medications` sits between `insights` and `thresholds`:
@@ -77,7 +78,6 @@ describe("SETTINGS_SECTION_SLUGS", () => {
"medications",
"mood",
"thresholds",
"sources",
"ai",
"api",
"export",
@@ -211,13 +211,11 @@ describe("<SettingsShell>", () => {
// About is back in the settings nav as the last entry (it also
// stays linked from the sidebar user-card dropdown).
expect(html).toContain('href="/settings/about"');
// v1.8.7.1 — Targets and Sources are two separate nav entries again.
// v1.18.0 (S3) — Targets keeps its own nav entry; Sources folded into
// Integrations as a sub-tab, so it is no longer a standalone nav entry.
expect(html).toContain('href="/settings/thresholds"');
expect(html).toContain('href="/settings/sources"');
expect(html).not.toContain('href="/settings/sources"');
expect(html).toContain("Targets");
// v1.12.0 — the source-priority entry adopts the canonical
// "Source priority" / "Quellen-Priorität" label (handover §5).
expect(html).toContain("Source priority");
});
it("resolves every section title via the i18n provider — German", () => {
@@ -235,10 +233,9 @@ describe("<SettingsShell>", () => {
expect(html).toContain("Layout &amp; Personalisierung");
expect(html).not.toContain('href="/settings/medications"');
expect(html).not.toContain('href="/settings/mood"');
// v1.8.7.1 — Targets and Sources are two separate German nav
// entries again: "Zielwerte" and "Quellen".
// v1.18.0 (S3) — Targets keeps its German nav entry ("Zielwerte");
// Sources folded into Integrations as a sub-tab.
expect(html).toContain("Zielwerte");
expect(html).toContain("Quellen");
// v1.8.7.1 — the AI Insights section is named "KI-Auswertungen" in
// German (the "KI" prefix makes the AI nature explicit).
expect(html).toContain("KI-Auswertungen");
@@ -248,8 +245,8 @@ describe("<SettingsShell>", () => {
// "Über diese App" (About) is back as the last in-shell nav entry
// (the sidebar user-card dropdown keeps its own link too).
expect(html).toContain('href="/settings/about"');
// v1.8.7.1 — both Targets and Sources nav entries are present.
expect(html).toContain('href="/settings/sources"');
// v1.18.0 (S3) — Sources is no longer a standalone nav entry.
expect(html).not.toContain('href="/settings/sources"');
});
it("does NOT surface the raw key when a translation resolves — guards against missing JSON entries", () => {
@@ -11,6 +11,10 @@
* per-channel status surface (moved here from Settings → Notifications,
* where it never belonged: channels are delivery providers, not
* reminder preferences). See `notification-channels-panel.tsx`.
* 3. Sources — the per-metric source-priority + device-type ladders that
* decide which connection wins when two report the same metric. Folded in
* from the former standalone `/settings/sources` page. See
* `sources-section.tsx` (sub-tab mode).
*
* `parseOAuthOutcome` / `oauthReasonKey` are re-exported from
* `connections-panel.tsx` so existing unit-test imports keep resolving.
@@ -21,6 +25,7 @@ import { useState } from "react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ConnectionsPanel } from "@/components/settings/integrations/connections-panel";
import { NotificationChannelsPanel } from "@/components/settings/integrations/notification-channels-panel";
import { SourcesSection } from "@/components/settings/sources-section";
import { useTranslations } from "@/lib/i18n/context";
export {
@@ -28,7 +33,7 @@ export {
oauthReasonKey,
} from "@/components/settings/integrations/connections-panel";
type IntegrationsTab = "connections" | "channels";
type IntegrationsTab = "connections" | "channels" | "sources";
export function IntegrationsSection() {
const { t } = useTranslations();
@@ -59,6 +64,9 @@ export function IntegrationsSection() {
<TabsTrigger value="channels">
{t("settings.sections.integrations.tabs.channels")}
</TabsTrigger>
<TabsTrigger value="sources">
{t("settings.sections.integrations.tabs.sources")}
</TabsTrigger>
</TabsList>
<TabsContent value="connections">
@@ -67,6 +75,9 @@ export function IntegrationsSection() {
<TabsContent value="channels">
<NotificationChannelsPanel />
</TabsContent>
<TabsContent value="sources">
<SourcesSection variant="subtab" />
</TabsContent>
</Tabs>
</section>
);
@@ -18,7 +18,6 @@ import {
Download,
Info,
KeyRound,
Layers,
LayoutDashboard,
Link2,
Pill,
@@ -47,7 +46,6 @@ const SLUG_ICON: Record<SettingsSectionSlug, LucideIcon> = {
medications: Pill,
mood: Smile,
thresholds: SlidersHorizontal,
sources: Layers,
ai: Sparkles,
api: KeyRound,
export: Download,
+6 -8
View File
@@ -6,13 +6,12 @@
* graph.
*/
// v1.8.7.1 — `thresholds` (Targets) and `sources` (Sources) are again two
// separate settings pages, each its own left-nav entry and route. They were
// merged into one "Targets & Sources" page in v1.4.34 IW-D, but the two
// editors drive distinct mutation flows (`/api/user/thresholds` vs
// `/api/auth/me/source-priority`) and reading them as one page made the
// surface long; splitting them back keeps each concern self-contained.
// Both slugs are served by the dynamic `[section]` route below.
// v1.18.0 (S3) — `sources` is no longer a standalone slug. Source priority
// folded into Settings → Integrations as the "Sources" sub-tab (deciding which
// connection wins when two report the same metric is an Integrations concern).
// `/settings/sources` 301-redirects to `/settings/integrations` (next.config.ts).
// `thresholds` (Targets) keeps its own left-nav entry + route, served by the
// dynamic `[section]` route below.
// v1.16.10 — `medications` sits after `insights`: the customise surface
// for the /medications list (view preference + manual order), following
// the dashboard / insights pattern of a dedicated settings section
@@ -46,7 +45,6 @@ export const SETTINGS_SECTION_SLUGS = [
"medications",
"mood",
"thresholds",
"sources",
"ai",
"api",
"export",
+4 -10
View File
@@ -24,7 +24,6 @@ import {
Download,
Info,
KeyRound,
Layers,
LayoutDashboard,
Link2,
Settings2,
@@ -74,10 +73,10 @@ interface SettingsSection {
* settings navigation. It is now both a regular (last) shell entry and
* a user-card dropdown item ("Über HealthLog" / "About HealthLog").
*
* v1.8.7.1 — `thresholds` (Targets) and `sources` (Sources) are two
* separate sidebar entries again (merged into a single "Targets &
* Sources" page in v1.4.34 IW-D, split back here). Both are served by
* the dynamic `[section]` route.
* v1.18.0 (S3) — `sources` is no longer a sidebar entry: source priority
* folded into Settings → Integrations as the "Sources" sub-tab.
* `thresholds` (Targets) keeps its own entry, served by the dynamic
* `[section]` route.
*/
export const SETTINGS_SECTIONS: readonly SettingsSection[] = [
{ slug: "account", titleKey: "settings.sections.account.title", icon: User },
@@ -122,11 +121,6 @@ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [
titleKey: "settings.sections.thresholds.title",
icon: SlidersHorizontal,
},
{
slug: "sources",
titleKey: "settings.sections.sources.title",
icon: Layers,
},
{ slug: "ai", titleKey: "settings.sections.ai.title", icon: Sparkles },
{ slug: "api", titleKey: "settings.sections.api.title", icon: KeyRound },
{
+46 -24
View File
@@ -111,18 +111,31 @@ const DEVICE_TYPE_LABEL_KEYS: Record<DeviceType, string> = {
unknown: "settings.sections.sources.deviceLabels.unknown",
};
export interface SourcesSectionProps {
/**
* `"standalone"` (the default) renders the full page header + the
* cross-link back to Integrations — the shape used while Sources had its
* own `/settings/sources` route. `"subtab"` (v1.18.0 S3) suppresses both:
* Sources now renders inside the Integrations → Sources sub-tab, so the
* page header is the parent section's and a cross-link back to the page
* that already contains it would be circular.
*/
variant?: "standalone" | "subtab";
}
/**
* `<SourcesSection>` — route-level wrapper for `/settings/sources`
* (the "Sources" / "Quellen" page).
* `<SourcesSection>` — the per-metric source-priority + device-type ladders.
*
* v1.8.7.1 — Sources is its own settings page again (it was merged into
* the combined "Targets & Sources" page under `/settings/thresholds` in
* v1.4.34 IW-D). The section renders its own page header (h1 + subtitle)
* and owns the per-metric source-priority + device-type ladders.
* v1.18.0 (S3) — Sources folded into Settings → Integrations as the "Sources"
* sub-tab (`variant="subtab"`), since deciding which connection wins when two
* report the same metric is an Integrations concern. The standalone
* `/settings/sources` route is gone; `/settings/sources` 301-redirects to
* `/settings/integrations` (see `next.config.ts`).
*/
export function SourcesSection() {
export function SourcesSection({ variant = "standalone" }: SourcesSectionProps = {}) {
const { t } = useTranslations();
const queryClient = useQueryClient();
const isSubtab = variant === "subtab";
const { data: remote, isLoading } = useQuery({
queryKey: queryKeys.sourcePriority(),
@@ -285,26 +298,35 @@ export function SourcesSection() {
aria-labelledby="settings-section-sources-title"
className="space-y-6"
>
<header className="space-y-1">
<h1 id="settings-section-sources-title" className="sr-only">
{t("settings.sections.sources.title")}
</h1>
{isSubtab ? (
// Sub-tab mode: the Integrations section owns the page header, and a
// cross-link back to the page that already contains this tab would be
// circular. Surface only the one-line description.
<p className="text-muted-foreground text-sm">
{t("settings.sections.sources.description")}
</p>
{/* Cross-link back to Settings → Integrations — the page that
actually adds / removes the sources this ladder ranks. */}
<p className="text-muted-foreground text-xs">
{t("settings.sections.sources.integrationsHint")}{" "}
<Link
href="/settings/integrations"
className="text-primary underline underline-offset-2"
data-slot="sources-integrations-cross-link"
>
{t("settings.sections.sources.integrationsHintLink")}
</Link>
</p>
</header>
) : (
<header className="space-y-1">
<h1 id="settings-section-sources-title" className="sr-only">
{t("settings.sections.sources.title")}
</h1>
<p className="text-muted-foreground text-sm">
{t("settings.sections.sources.description")}
</p>
{/* Cross-link back to Settings → Integrations — the page that
actually adds / removes the sources this ladder ranks. */}
<p className="text-muted-foreground text-xs">
{t("settings.sections.sources.integrationsHint")}{" "}
<Link
href="/settings/integrations"
className="text-primary underline underline-offset-2"
data-slot="sources-integrations-cross-link"
>
{t("settings.sections.sources.integrationsHintLink")}
</Link>
</p>
</header>
)}
<div className="bg-card border-border space-y-4 rounded-xl border p-4 sm:p-6">
<SettingsCardHeader