mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 06:32:03 +00:00
feat(settings): split targets and sources, rename AI Insights section
Three related settings changes: - Split the combined Targets & Sources page back into two pages: Targets (/settings/thresholds) owns the per-metric target-range editor and Sources (/settings/sources) owns the source-priority and device-type ladders. Each is its own left-nav entry and route, served by the dynamic [section] route. The former /settings/sources permanent redirect is removed since the route now renders the page directly. - Rename the AI Insights settings section and nav label so the AI nature is explicit: 'AI Insights' (EN) / 'KI-Auswertungen' (DE), replacing the bare 'Insights' / 'Auswertungen'. - Each page renders its own h1 heading consistent with sibling settings pages. Drop the now-unused embedded mode from SourcesSection. Update the section titles and descriptions across all six locale bundles. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> (cherry picked from commit 5d8842934b985eb8ce037993a63bb0736ac0c7bd)
This commit is contained in:
1 parent
708d012d28
commit
78b76d2ac5
14 files changed
+122
-167
No files matched your search
@@ -11,11 +11,12 @@ import { ExportSection } from "@/components/settings/export-section";
|
||||
import { IntegrationsSection } from "@/components/settings/integrations-section";
|
||||
import { NotificationsSection } from "@/components/settings/notifications-section";
|
||||
import { SectionPlaceholder } from "@/components/settings/section-placeholder";
|
||||
// v1.4.34 IW-D — `sources` slug was retired (merged into `thresholds`).
|
||||
// The combined "Targets & Sources" component lives in
|
||||
// `thresholds-section.tsx`; `/settings/sources` is now a
|
||||
// `permanentRedirect` handled by the dedicated route file.
|
||||
// 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.
|
||||
import { ThresholdsSection } from "@/components/settings/thresholds-section";
|
||||
import { SourcesSection } from "@/components/settings/sources-section";
|
||||
import {
|
||||
SETTINGS_SECTION_SLUGS,
|
||||
isSettingsSectionSlug,
|
||||
@@ -49,6 +50,7 @@ const SECTION_COMPONENTS: Record<
|
||||
notifications: NotificationsSection,
|
||||
dashboard: DashboardSection,
|
||||
thresholds: ThresholdsSection,
|
||||
sources: () => <SourcesSection />,
|
||||
api: ApiSection,
|
||||
export: ExportSection,
|
||||
advanced: AdvancedSection,
|
||||
|
||||
@@ -79,15 +79,6 @@ vi.mock("@/components/settings/dashboard-layout-section", () => ({
|
||||
vi.mock("@/components/settings/thresholds-editor-section", () => ({
|
||||
ThresholdsEditorSection: () => <div data-testid="thresholds-editor" />,
|
||||
}));
|
||||
// v1.4.34 IW-D — `<ThresholdsSection>` now also embeds
|
||||
// `<SourcesSection mode="embedded">` so the combined "Targets &
|
||||
// Sources" page reads as one shelf. The SourcesSection itself owns
|
||||
// React Query and TanStack mutation wiring; stub it at the import
|
||||
// boundary to keep the SSR smoke test focused on the wrapper.
|
||||
vi.mock("@/components/settings/sources-section", () => ({
|
||||
SourcesSection: () => <div data-testid="sources-section" />,
|
||||
}));
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { AboutSection } from "../about-section";
|
||||
import { AccountSection } from "../account-section";
|
||||
@@ -169,12 +160,12 @@ describe("settings sections — SSR smoke", () => {
|
||||
expect(html).toContain("Tour ansehen");
|
||||
});
|
||||
|
||||
it("<AiSection> renders the Insights card", () => {
|
||||
it("<AiSection> renders the AI Insights heading", () => {
|
||||
const html = render(<AiSection />);
|
||||
// v1.4.33 IW7 — section renamed from "AI Insights" to "Insights"
|
||||
// per the Marc-Voice rule. The card title is replaced with a sparkles
|
||||
// icon + status badges, and the section H1 carries the only heading.
|
||||
expect(html).toContain("Insights");
|
||||
// v1.8.7.1 — section renamed to "AI Insights" so the AI nature of the
|
||||
// surface is explicit (was the bare "Insights"). The section H1 carries
|
||||
// the only heading.
|
||||
expect(html).toContain("AI Insights");
|
||||
expect(html).toContain("settings-section-ai-title");
|
||||
});
|
||||
|
||||
@@ -230,26 +221,26 @@ describe("settings sections — SSR smoke", () => {
|
||||
expect(html).not.toContain(">Jetzt prüfen<");
|
||||
});
|
||||
|
||||
it("<ThresholdsSection> renders heading and embeds both editors (v1.4.34 IW-D merge)", () => {
|
||||
it("<ThresholdsSection> renders the Targets heading and the threshold editor only", () => {
|
||||
// v1.4.16 phase B6 contract: the route wrapper is named
|
||||
// `<ThresholdsSection>` (was `<ThresholdsSettingsSection>`), lives
|
||||
// at `thresholds-section.tsx`, and embeds the inner editor under
|
||||
// `<ThresholdsEditorSection>` (was `<ThresholdsSection>`).
|
||||
// v1.4.34 IW-D — the wrapper now also embeds
|
||||
// `<SourcesSection mode="embedded">` so the page is the merged
|
||||
// "Targets & Sources" surface. Both editor mounts must be present.
|
||||
// v1.8.7.1 — Sources split back onto its own page, so this wrapper
|
||||
// renders the target-range editor only.
|
||||
const html = render(<ThresholdsSection />);
|
||||
// v1.4.34 IW-D: section renamed to "Targets & Sources".
|
||||
expect(html).toContain("Targets & Sources");
|
||||
// v1.8.7.1: section renamed to "Targets".
|
||||
expect(html).toContain("Targets");
|
||||
expect(html).toContain('data-testid="thresholds-editor"');
|
||||
expect(html).toContain('data-testid="sources-section"');
|
||||
expect(html).not.toContain('data-testid="sources-section"');
|
||||
expect(html).not.toContain("settings.sections.thresholds.");
|
||||
});
|
||||
|
||||
it("<ThresholdsSection> resolves the German title (B6 rename + IW-D merge)", () => {
|
||||
it("<ThresholdsSection> resolves the German title", () => {
|
||||
const html = render(<ThresholdsSection />, "de");
|
||||
expect(html).toContain("Zielwerte & Quellen");
|
||||
// v1.8.7.1 — German title is "Zielwerte" (Targets only).
|
||||
expect(html).toContain("Zielwerte");
|
||||
expect(html).toContain('data-testid="thresholds-editor"');
|
||||
expect(html).toContain('data-testid="sources-section"');
|
||||
expect(html).not.toContain('data-testid="sources-section"');
|
||||
});
|
||||
});
|
||||
@@ -35,7 +35,7 @@ function renderShell(props: {
|
||||
}
|
||||
|
||||
describe("SETTINGS_SECTION_SLUGS", () => {
|
||||
it("declares the ten sections agreed for v1.4.34", () => {
|
||||
it("declares the eleven sections", () => {
|
||||
// Order matters — `generateStaticParams()` and the sidebar derive their
|
||||
// ordering from this constant, so a reorder is a behaviour change.
|
||||
// v1.4.3 split the dashboard panel: layout stays under `dashboard`,
|
||||
@@ -44,19 +44,17 @@ describe("SETTINGS_SECTION_SLUGS", () => {
|
||||
// v1.4.16 phase B7 added the consolidated `export` section between
|
||||
// `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` so the
|
||||
// per-metric-class source-priority surface lives next to the other
|
||||
// analytics-shaping controls (thresholds → ranges; sources →
|
||||
// canonical source per metric).
|
||||
// v1.4.34 IW-D — `sources` slug was retired (merged into
|
||||
// `thresholds`, now "Targets & Sources"). `/settings/sources` stays
|
||||
// alive as a `permanentRedirect`. Section count: 10 (was 11).
|
||||
// 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`. Section count: 11.
|
||||
expect([...SETTINGS_SECTION_SLUGS]).toEqual([
|
||||
"account",
|
||||
"integrations",
|
||||
"notifications",
|
||||
"dashboard",
|
||||
"thresholds",
|
||||
"sources",
|
||||
"ai",
|
||||
"api",
|
||||
"export",
|
||||
@@ -164,11 +162,11 @@ describe("<SettingsShell>", () => {
|
||||
// in the sidebar user-card dropdown. Route `/settings/about` is
|
||||
// still alive for direct links.
|
||||
expect(html).not.toContain('href="/settings/about"');
|
||||
// v1.4.34 IW-D — Sources slug retired; merged into Targets & Sources
|
||||
// under `/settings/thresholds`. The Sources nav entry must not
|
||||
// appear; the route stays alive via permanentRedirect.
|
||||
expect(html).not.toContain('href="/settings/sources"');
|
||||
expect(html).toContain("Targets & Sources");
|
||||
// v1.8.7.1 — Targets and Sources are two separate nav entries again.
|
||||
expect(html).toContain('href="/settings/thresholds"');
|
||||
expect(html).toContain('href="/settings/sources"');
|
||||
expect(html).toContain("Targets");
|
||||
expect(html).toContain("Sources");
|
||||
});
|
||||
|
||||
it("resolves every section title via the i18n provider — German", () => {
|
||||
@@ -184,21 +182,21 @@ describe("<SettingsShell>", () => {
|
||||
// per-metric overrides moved out into their own "Persönliche Zielwerte"
|
||||
// section, which is the new entry below the Dashboard one.
|
||||
expect(html).toContain("Dashboard");
|
||||
// v1.4.34 IW-D — section renamed from "Persönliche Zielwerte" to
|
||||
// "Zielwerte & Quellen" (combined with the former Sources section).
|
||||
expect(html).toContain("Zielwerte & Quellen");
|
||||
// v1.4.33 IW7 — "KI-Auswertungen" renamed to "Auswertungen" per
|
||||
// the Marc-Voice rule (no "KI"/"AI" prefix in user-facing copy).
|
||||
expect(html).toContain("Auswertungen");
|
||||
// v1.8.7.1 — Targets and Sources are two separate German nav
|
||||
// entries again: "Zielwerte" and "Quellen".
|
||||
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");
|
||||
// API & Tokens is identical in both locales (proper noun + ampersand)
|
||||
expect(html).toContain("API & Tokens");
|
||||
expect(html).toContain("Erweitert");
|
||||
// v1.4.33 IW7 — "Über" (About) section removed from the in-shell
|
||||
// nav, folded into the sidebar user-card dropdown ("Über HealthLog").
|
||||
expect(html).not.toContain('href="/settings/about"');
|
||||
// v1.4.34 IW-D — Sources nav entry retired (merged into Targets &
|
||||
// Sources).
|
||||
expect(html).not.toContain('href="/settings/sources"');
|
||||
// v1.8.7.1 — both Targets and Sources nav entries are present.
|
||||
expect(html).toContain('href="/settings/sources"');
|
||||
});
|
||||
|
||||
it("does NOT surface the raw key when a translation resolves — guards against missing JSON entries", () => {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
Download,
|
||||
Info,
|
||||
KeyRound,
|
||||
Layers,
|
||||
LayoutDashboard,
|
||||
Link2,
|
||||
Settings2,
|
||||
@@ -35,6 +36,7 @@ const SLUG_ICON: Record<SettingsSectionSlug, LucideIcon> = {
|
||||
notifications: Bell,
|
||||
dashboard: LayoutDashboard,
|
||||
thresholds: SlidersHorizontal,
|
||||
sources: Layers,
|
||||
ai: Sparkles,
|
||||
api: KeyRound,
|
||||
export: Download,
|
||||
|
||||
@@ -6,20 +6,20 @@
|
||||
* graph.
|
||||
*/
|
||||
|
||||
// v1.4.34 IW-D — `sources` slug was retired. The two former sections
|
||||
// (`/settings/thresholds` and `/settings/sources`) merged into a single
|
||||
// "Targets & Sources" page under `/settings/thresholds`. The historic
|
||||
// `/settings/sources` URL stays alive as a `permanentRedirect` (see
|
||||
// `src/app/settings/sources/page.tsx`) so external bookmarks (iOS,
|
||||
// docs) follow through unchanged. Per
|
||||
// `.planning/research/v1434-r-2-carryover-scope.md` §7 and
|
||||
// `.planning/round-v1433-audit-menu.md` §7.1 (item 3).
|
||||
// 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.
|
||||
export const SETTINGS_SECTION_SLUGS = [
|
||||
"account",
|
||||
"integrations",
|
||||
"notifications",
|
||||
"dashboard",
|
||||
"thresholds",
|
||||
"sources",
|
||||
"ai",
|
||||
"api",
|
||||
"export",
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
Bell,
|
||||
Download,
|
||||
KeyRound,
|
||||
Layers,
|
||||
LayoutDashboard,
|
||||
Link2,
|
||||
Settings2,
|
||||
@@ -74,14 +75,10 @@ interface SettingsSection {
|
||||
* navigation. Direct links (the user-card dropdown, the public
|
||||
* `/about` permalink) continue to resolve to the same component.
|
||||
*
|
||||
* v1.4.34 IW-D — `sources` was merged into `thresholds` (now titled
|
||||
* "Targets & Sources" / "Zielwerte & Quellen"). The combined section
|
||||
* surfaces per-metric source priority alongside the threshold inputs
|
||||
* on a single page. `/settings/sources` stays alive as a
|
||||
* `permanentRedirect` so external bookmarks keep working; the slug
|
||||
* was dropped from `SETTINGS_SECTION_SLUGS` because the dynamic
|
||||
* `[section]` route no longer serves it. Per
|
||||
* `.planning/research/v1434-r-2-carryover-scope.md` §7.
|
||||
* 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.
|
||||
*/
|
||||
export const SETTINGS_SECTIONS: readonly SettingsSection[] = [
|
||||
{ slug: "account", titleKey: "settings.sections.account.title", icon: User },
|
||||
@@ -105,6 +102,11 @@ 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 },
|
||||
{
|
||||
|
||||
@@ -99,25 +99,15 @@ const DEVICE_TYPE_LABEL_KEYS: Record<DeviceType, string> = {
|
||||
};
|
||||
|
||||
/**
|
||||
* v1.4.34 IW-D — section presentation mode.
|
||||
* - `"standalone"` (default) renders the page-level header (h1 +
|
||||
* subtitle) and keeps the section's own aria-labelledby wiring.
|
||||
* - `"embedded"` drops the header because a parent surface
|
||||
* (`<ThresholdsSection>` on `/settings/thresholds`) already names
|
||||
* the page. The section becomes a `<div>` instead of a
|
||||
* `<section>` so the assistive-tech reading order stays one
|
||||
* combined surface.
|
||||
* `<SourcesSection>` — route-level wrapper for `/settings/sources`
|
||||
* (the "Sources" / "Quellen" page).
|
||||
*
|
||||
* `/settings/sources` no longer renders this component directly — it
|
||||
* permanently redirects to `/settings/thresholds`, which uses the
|
||||
* `embedded` mode. The default keeps backward compatibility for any
|
||||
* test/storybook that still mounts `<SourcesSection />` standalone.
|
||||
* 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.
|
||||
*/
|
||||
export interface SourcesSectionProps {
|
||||
mode?: "standalone" | "embedded";
|
||||
}
|
||||
|
||||
export function SourcesSection({ mode = "standalone" }: SourcesSectionProps = {}) {
|
||||
export function SourcesSection() {
|
||||
const { t } = useTranslations();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -292,32 +282,22 @@ export function SourcesSection({ mode = "standalone" }: SourcesSectionProps = {}
|
||||
return Object.keys(priority.deviceTypePriority).length;
|
||||
}, [priority]);
|
||||
|
||||
// v1.4.34 IW-D — `embedded` mode skips the section header because the
|
||||
// parent surface (`<ThresholdsSection>`) already names the page.
|
||||
const isStandalone = mode === "standalone";
|
||||
const Wrapper = isStandalone ? "section" : "div";
|
||||
const wrapperProps = isStandalone
|
||||
? {
|
||||
"aria-labelledby": "settings-section-sources-title",
|
||||
className: "space-y-6",
|
||||
}
|
||||
: { className: "space-y-6" };
|
||||
|
||||
return (
|
||||
<Wrapper {...wrapperProps}>
|
||||
{isStandalone && (
|
||||
<header className="space-y-1">
|
||||
<h1
|
||||
id="settings-section-sources-title"
|
||||
className="text-2xl font-semibold tracking-tight"
|
||||
>
|
||||
{t("settings.sections.sources.title")}
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("settings.sections.sources.description")}
|
||||
</p>
|
||||
</header>
|
||||
)}
|
||||
<section
|
||||
aria-labelledby="settings-section-sources-title"
|
||||
className="space-y-6"
|
||||
>
|
||||
<header className="space-y-1">
|
||||
<h1
|
||||
id="settings-section-sources-title"
|
||||
className="text-2xl font-semibold tracking-tight"
|
||||
>
|
||||
{t("settings.sections.sources.title")}
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("settings.sections.sources.description")}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="bg-card border-border space-y-4 rounded-xl border p-6">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3">
|
||||
@@ -599,7 +579,7 @@ export function SourcesSection({ mode = "standalone" }: SourcesSectionProps = {}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Wrapper>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,37 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { SourcesSection } from "@/components/settings/sources-section";
|
||||
import { ThresholdsEditorSection } from "@/components/settings/thresholds-editor-section";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
/**
|
||||
* `<ThresholdsSection>` — route-level wrapper for `/settings/thresholds`.
|
||||
* `<ThresholdsSection>` — route-level wrapper for `/settings/thresholds`
|
||||
* (the "Targets" / "Zielwerte" page).
|
||||
*
|
||||
* v1.4.34 IW-D — section was renamed "Zielwerte & Quellen" / "Targets &
|
||||
* Sources" and absorbs the former `/settings/sources` content. The two
|
||||
* editors stay distinct mutation flows (`/api/user/thresholds` for
|
||||
* ranges + `/api/auth/me/source-priority` for source ladders) so a save
|
||||
* on one half never disturbs the other; presenting them on a single
|
||||
* page collapses two adjacent sidebar slots into one and matches the
|
||||
* Settings nav audit (§7.1 item 3 in
|
||||
* `.planning/round-v1433-audit-menu.md` — both screens were per-metric
|
||||
* configuration shelves and reading them together matches how a user
|
||||
* actually thinks about a metric: "for this metric, which sources do
|
||||
* I trust AND what range counts as healthy?").
|
||||
*
|
||||
* `/settings/sources` stays alive as a `permanentRedirect` (see
|
||||
* `src/app/settings/sources/page.tsx`) so external bookmarks (iOS
|
||||
* Settings deep-links, docs) keep resolving.
|
||||
* v1.8.7.1 — Targets and Sources are two separate settings pages again.
|
||||
* This page owns only the per-metric target-range editor
|
||||
* (`/api/user/thresholds`); the source-priority ladders moved back onto
|
||||
* their own `/settings/sources` page (`<SourcesSection>`). The two were
|
||||
* merged into one "Targets & Sources" page in v1.4.34 IW-D, but the
|
||||
* combined surface ran long and the two editors drive distinct mutation
|
||||
* flows, so each concern is self-contained on its own page.
|
||||
*
|
||||
* Visual structure on this page:
|
||||
* 1. Section header (combined title + description).
|
||||
* 2. `<ThresholdsEditorSection>` — per-metric range inputs (top).
|
||||
* The editor card kept its own affordance row (icon, reset-all
|
||||
* button) so the action surface stays compact.
|
||||
* 3. `<SourcesSection>` body — per-metric source priority + the
|
||||
* two-axis device-type expander (bottom). The inner section's
|
||||
* header was suppressed in `mode="embedded"` because the page
|
||||
* header above already names the surface.
|
||||
* 1. Section header (title + description).
|
||||
* 2. `<ThresholdsEditorSection>` — per-metric range inputs. The editor
|
||||
* card keeps its own affordance row (icon, reset-all button).
|
||||
*
|
||||
* v1.4.16 phase B6: file renamed from the historic
|
||||
* `thresholds-settings-section.tsx` (`<ThresholdsSettingsSection>`)
|
||||
@@ -63,15 +50,8 @@ export function ThresholdsSection() {
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{/* Per-metric threshold ranges — top card. */}
|
||||
{/* Per-metric threshold ranges. */}
|
||||
<ThresholdsEditorSection id="thresholds" />
|
||||
|
||||
{/* Per-metric source priority + two-axis device-type ladder —
|
||||
bottom card. `mode="embedded"` suppresses the inner h1 +
|
||||
subtitle (the page header above owns the surface name) and
|
||||
drops the inner `<section>` aria-label so the assistive-tech
|
||||
reading order stays one combined surface, not two. */}
|
||||
<SourcesSection mode="embedded" />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user