diff --git a/next.config.ts b/next.config.ts
index fd22ff481..a279dd6e0 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -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() {
diff --git a/src/app/settings/[section]/page.tsx b/src/app/settings/[section]/page.tsx
index 645e28a1a..0d5e60a15 100644
--- a/src/app/settings/[section]/page.tsx
+++ b/src/app/settings/[section]/page.tsx
@@ -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: () => ,
api: ApiSection,
export: ExportSection,
advanced: AdvancedSection,
diff --git a/src/components/settings/__tests__/settings-shell.test.tsx b/src/components/settings/__tests__/settings-shell.test.tsx
index ddca9005d..e9513b823 100644
--- a/src/components/settings/__tests__/settings-shell.test.tsx
+++ b/src/components/settings/__tests__/settings-shell.test.tsx
@@ -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("", () => {
// 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("", () => {
expect(html).toContain("Layout & 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("", () => {
// "Ü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", () => {
diff --git a/src/components/settings/integrations-section.tsx b/src/components/settings/integrations-section.tsx
index dfebc9b88..86e6d5ad0 100644
--- a/src/components/settings/integrations-section.tsx
+++ b/src/components/settings/integrations-section.tsx
@@ -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() {
{t("settings.sections.integrations.tabs.channels")}
+
+ {t("settings.sections.integrations.tabs.sources")}
+
@@ -67,6 +75,9 @@ export function IntegrationsSection() {
+
+
+
);
diff --git a/src/components/settings/section-placeholder.tsx b/src/components/settings/section-placeholder.tsx
index 846d3c3af..9bf8765b0 100644
--- a/src/components/settings/section-placeholder.tsx
+++ b/src/components/settings/section-placeholder.tsx
@@ -18,7 +18,6 @@ import {
Download,
Info,
KeyRound,
- Layers,
LayoutDashboard,
Link2,
Pill,
@@ -47,7 +46,6 @@ const SLUG_ICON: Record = {
medications: Pill,
mood: Smile,
thresholds: SlidersHorizontal,
- sources: Layers,
ai: Sparkles,
api: KeyRound,
export: Download,
diff --git a/src/components/settings/section-slugs.ts b/src/components/settings/section-slugs.ts
index b2e65c233..96dc02271 100644
--- a/src/components/settings/section-slugs.ts
+++ b/src/components/settings/section-slugs.ts
@@ -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",
diff --git a/src/components/settings/settings-shell.tsx b/src/components/settings/settings-shell.tsx
index 1c5c14309..12f28598a 100644
--- a/src/components/settings/settings-shell.tsx
+++ b/src/components/settings/settings-shell.tsx
@@ -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 },
{
diff --git a/src/components/settings/sources-section.tsx b/src/components/settings/sources-section.tsx
index eb5b44310..e490ae727 100644
--- a/src/components/settings/sources-section.tsx
+++ b/src/components/settings/sources-section.tsx
@@ -111,18 +111,31 @@ const DEVICE_TYPE_LABEL_KEYS: Record = {
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";
+}
+
/**
- * `` — route-level wrapper for `/settings/sources`
- * (the "Sources" / "Quellen" page).
+ * `` — 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"
>
-
-
- {t("settings.sections.sources.title")}
-
+ {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.
{t("settings.sections.sources.description")}
- {/* Cross-link back to Settings → Integrations — the page that
- actually adds / removes the sources this ladder ranks. */}
-