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. */} -

- {t("settings.sections.sources.integrationsHint")}{" "} - - {t("settings.sections.sources.integrationsHintLink")} - -

-
+ ) : ( +
+

+ {t("settings.sections.sources.title")} +

+

+ {t("settings.sections.sources.description")} +

+ {/* Cross-link back to Settings → Integrations — the page that + actually adds / removes the sources this ladder ranks. */} +

+ {t("settings.sections.sources.integrationsHint")}{" "} + + {t("settings.sections.sources.integrationsHintLink")} + +

+
+ )}