diff --git a/messages/de.json b/messages/de.json
index 834c964cb..b80506688 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -890,8 +890,12 @@
"description": "Telegram, ntfy, Web Push."
},
"dashboard": {
- "title": "Übersicht",
- "description": "Kachel-Layout, Schwellwerte."
+ "title": "Dashboard",
+ "description": "Kachel-Layout und Reihenfolge."
+ },
+ "thresholds": {
+ "title": "Persönliche Zielwerte",
+ "description": "Eigene Zielbereiche für jede Metrik."
},
"ai": {
"title": "KI-Auswertungen",
diff --git a/messages/en.json b/messages/en.json
index 0b2704c5d..2171969f1 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -891,7 +891,11 @@
},
"dashboard": {
"title": "Dashboard",
- "description": "Tile layout, thresholds."
+ "description": "Tile layout and order."
+ },
+ "thresholds": {
+ "title": "Personal targets",
+ "description": "Override target ranges for every metric."
},
"ai": {
"title": "AI Insights",
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 456cc3936..3a85e8e18 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -460,66 +460,50 @@ export default function DashboardPage() {
});
}
if (showBpCards) {
+ // BP renders as ONE tile with paired sys/dia values (e.g. "117/79")
+ // so the strip width matches the chart strip below — and so the
+ // tile count stays at 5 instead of 6 with a wrap-around. The
+ // sparkline/trend arrow follows the systolic value (the common
+ // primary signal); the colour-banding tooltips cover sys only
+ // because dia uses a different target range and stuffing both
+ // into one hint became unreadable on mobile.
trendCards.push({
id: "bp",
order: widgetOrder("bp"),
node: (
-
-
-
-
+
),
});
}
@@ -868,14 +852,14 @@ export default function DashboardPage() {
* touch.
*/}
{trendCards.map((entry) => (
{entry.node}
diff --git a/src/app/settings/[section]/page.tsx b/src/app/settings/[section]/page.tsx
index 31234c0b9..2e4ab29bd 100644
--- a/src/app/settings/[section]/page.tsx
+++ b/src/app/settings/[section]/page.tsx
@@ -10,6 +10,7 @@ import { DashboardSection } from "@/components/settings/dashboard-section";
import { IntegrationsSection } from "@/components/settings/integrations-section";
import { NotificationsSection } from "@/components/settings/notifications-section";
import { SectionPlaceholder } from "@/components/settings/section-placeholder";
+import { ThresholdsSettingsSection } from "@/components/settings/thresholds-settings-section";
import {
SETTINGS_SECTION_SLUGS,
isSettingsSectionSlug,
@@ -42,6 +43,7 @@ const SECTION_COMPONENTS: Record<
integrations: IntegrationsSection,
notifications: NotificationsSection,
dashboard: DashboardSection,
+ thresholds: ThresholdsSettingsSection,
api: ApiSection,
advanced: AdvancedSection,
};
diff --git a/src/components/charts/trend-card.tsx b/src/components/charts/trend-card.tsx
index 7ddbffce1..4ca248a9a 100644
--- a/src/components/charts/trend-card.tsx
+++ b/src/components/charts/trend-card.tsx
@@ -11,6 +11,13 @@ import {
} from "@/components/ui/tooltip";
import { useTranslations, useFormatters } from "@/lib/i18n/context";
+interface SecondaryMetric {
+ /** Sub-value latest reading (e.g. diastolic when latest is systolic). */
+ latest: number | null;
+ avg7: number | null;
+ avg30: number | null;
+}
+
interface TrendCardProps {
label: string;
latest: number | null;
@@ -23,6 +30,10 @@ interface TrendCardProps {
avg30Hint?: React.ReactNode;
slope30: TrendSlope | null;
icon: React.ComponentType<{ className?: string }>;
+ /** Optional second value rendered next to the primary as `X / Y` (used for
+ * paired metrics like blood-pressure systolic/diastolic so a single tile
+ * shows both numbers). */
+ secondary?: SecondaryMetric;
}
export function TrendCard({
@@ -37,6 +48,7 @@ export function TrendCard({
avg30Hint,
slope30,
icon: Icon,
+ secondary,
}: TrendCardProps) {
const { t } = useTranslations();
const fmt = useFormatters();
@@ -58,8 +70,19 @@ export function TrendCard({
const formatValue = (value: number) => fmt.number(value, 1);
+ const renderPair = (
+ primary: number | null,
+ secondaryValue: number | null | undefined,
+ ): string => {
+ if (primary === null) return "—";
+ if (secondary && secondaryValue !== null && secondaryValue !== undefined) {
+ return `${formatValue(primary)}/${formatValue(secondaryValue)}`;
+ }
+ return formatValue(primary);
+ };
+
return (
-
+
{label}
@@ -68,13 +91,15 @@ export function TrendCard({
- {latest !== null ? formatValue(latest) : "—"}
+ {latest !== null
+ ? renderPair(latest, secondary?.latest)
+ : "—"}
{unit}
{slope30 && }
-
+
{avg7 !== null && (
{t("charts.avg7dShort")}:{" "}
@@ -82,7 +107,7 @@ export function TrendCard({
- {formatValue(avg7)}
+ {renderPair(avg7, secondary?.avg7)}
@@ -91,7 +116,7 @@ export function TrendCard({
) : (
- {formatValue(avg7)}
+ {renderPair(avg7, secondary?.avg7)}
)}
@@ -103,7 +128,7 @@ export function TrendCard({
- {formatValue(avg30)}
+ {renderPair(avg30, secondary?.avg30)}
@@ -112,7 +137,7 @@ export function TrendCard({
) : (
- {formatValue(avg30)}
+ {renderPair(avg30, secondary?.avg30)}
)}
diff --git a/src/components/settings/__tests__/sections.test.tsx b/src/components/settings/__tests__/sections.test.tsx
index 7039fd19f..2eb944cc2 100644
--- a/src/components/settings/__tests__/sections.test.tsx
+++ b/src/components/settings/__tests__/sections.test.tsx
@@ -122,11 +122,16 @@ describe("settings sections — SSR smoke", () => {
expect(html).toContain("Notifications");
});
- it("
renders heading and embeds layout + thresholds", () => {
+ it(" renders heading and embeds the layout customizer", () => {
+ // v1.4.3 split: the per-metric thresholds (Persönliche Zielwerte) moved
+ // out into their own settings section under `/settings/thresholds`,
+ // so the dashboard panel is now layout-only. Verifying the heading
+ // says "Dashboard" (not "Übersicht" — the v1.4.2 placeholder) and the
+ // layout slot still mounts.
const html = render();
expect(html).toContain("Dashboard");
expect(html).toContain('data-testid="dashboard-layout"');
- expect(html).toContain('data-testid="thresholds"');
+ expect(html).not.toContain('data-testid="thresholds"');
});
it(" renders endpoints + tokens cards", () => {
diff --git a/src/components/settings/__tests__/settings-shell.test.tsx b/src/components/settings/__tests__/settings-shell.test.tsx
index 06c1a9fcc..eb5d257f3 100644
--- a/src/components/settings/__tests__/settings-shell.test.tsx
+++ b/src/components/settings/__tests__/settings-shell.test.tsx
@@ -35,14 +35,18 @@ function renderShell(props: {
}
describe("SETTINGS_SECTION_SLUGS", () => {
- it("declares exactly the eight sections agreed for v1.4", () => {
+ it("declares exactly the nine sections agreed for v1.4.3", () => {
// 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`,
+ // per-metric overrides moved to their own `thresholds` slug so it's
+ // a top-level entry in the settings nav.
expect([...SETTINGS_SECTION_SLUGS]).toEqual([
"account",
"integrations",
"notifications",
"dashboard",
+ "thresholds",
"ai",
"api",
"advanced",
@@ -67,7 +71,7 @@ describe("SETTINGS_SECTION_SLUGS", () => {
});
describe("", () => {
- it("renders all eight section links — once for the mobile strip and once for the desktop sidebar", () => {
+ it("renders every section link — once for the mobile strip and once for the desktop sidebar", () => {
const html = renderShell({ active: "account" });
for (const slug of SETTINGS_SECTION_SLUGS) {
const matches = html.match(new RegExp(`href="/settings/${slug}"`, "g"));
@@ -138,7 +142,12 @@ describe("", () => {
expect(html).toContain("Konto");
expect(html).toContain("Integrationen");
expect(html).toContain("Benachrichtigungen");
- expect(html).toContain("Übersicht");
+ // v1.4.3: the Settings sub-section formerly labelled "Übersicht" is now
+ // "Dashboard" (matching the term users see in the main nav). The
+ // 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");
+ expect(html).toContain("Persönliche Zielwerte");
expect(html).toContain("KI-Auswertungen");
// API & Tokens is identical in both locales (proper noun + ampersand)
expect(html).toContain("API & Tokens");
diff --git a/src/components/settings/dashboard-section.tsx b/src/components/settings/dashboard-section.tsx
index 90a8b1728..6914aaaac 100644
--- a/src/components/settings/dashboard-section.tsx
+++ b/src/components/settings/dashboard-section.tsx
@@ -1,7 +1,6 @@
"use client";
import { DashboardLayoutSection } from "@/components/settings/dashboard-layout-section";
-import { ThresholdsSection } from "@/components/settings/thresholds-section";
import { useTranslations } from "@/lib/i18n/context";
export function DashboardSection() {
@@ -25,7 +24,6 @@ export function DashboardSection() {
-
);
}
diff --git a/src/components/settings/section-placeholder.tsx b/src/components/settings/section-placeholder.tsx
index 5aff0d212..7e63de08c 100644
--- a/src/components/settings/section-placeholder.tsx
+++ b/src/components/settings/section-placeholder.tsx
@@ -18,6 +18,7 @@ import {
LayoutDashboard,
Link2,
Settings2,
+ SlidersHorizontal,
Sparkles,
User,
type LucideIcon,
@@ -32,6 +33,7 @@ const SLUG_ICON: Record = {
integrations: Link2,
notifications: Bell,
dashboard: LayoutDashboard,
+ thresholds: SlidersHorizontal,
ai: Sparkles,
api: KeyRound,
advanced: Settings2,
diff --git a/src/components/settings/section-slugs.ts b/src/components/settings/section-slugs.ts
index 7198be4ea..5a69cbf0f 100644
--- a/src/components/settings/section-slugs.ts
+++ b/src/components/settings/section-slugs.ts
@@ -11,6 +11,7 @@ export const SETTINGS_SECTION_SLUGS = [
"integrations",
"notifications",
"dashboard",
+ "thresholds",
"ai",
"api",
"advanced",
diff --git a/src/components/settings/settings-shell.tsx b/src/components/settings/settings-shell.tsx
index d329e3bbd..6434d543b 100644
--- a/src/components/settings/settings-shell.tsx
+++ b/src/components/settings/settings-shell.tsx
@@ -24,6 +24,7 @@ import {
LayoutDashboard,
Link2,
Settings2,
+ SlidersHorizontal,
Sparkles,
User,
type LucideIcon,
@@ -79,6 +80,11 @@ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [
titleKey: "settings.sections.dashboard.title",
icon: LayoutDashboard,
},
+ {
+ slug: "thresholds",
+ titleKey: "settings.sections.thresholds.title",
+ icon: SlidersHorizontal,
+ },
{ slug: "ai", titleKey: "settings.sections.ai.title", icon: Sparkles },
{ slug: "api", titleKey: "settings.sections.api.title", icon: KeyRound },
{
diff --git a/src/components/settings/thresholds-settings-section.tsx b/src/components/settings/thresholds-settings-section.tsx
new file mode 100644
index 000000000..e55f745da
--- /dev/null
+++ b/src/components/settings/thresholds-settings-section.tsx
@@ -0,0 +1,29 @@
+"use client";
+
+import { ThresholdsSection } from "@/components/settings/thresholds-section";
+import { useTranslations } from "@/lib/i18n/context";
+
+export function ThresholdsSettingsSection() {
+ const { t } = useTranslations();
+
+ return (
+
+ );
+}