]*\bspace-y-8\b/);
- });
-});
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 6da5b170f..f9f989b65 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -466,7 +466,8 @@ export default function DashboardPage() {
* misleading zero. The corrected gate requires at least one window
* (7d, 30d, all-time) to report a non-zero share. When every
* window is zero, the tile is hidden — the user sees the BP charts
- * + the `/targets` Blutdruck card for the deeper analysis instead.
+ * + the Insights blood-pressure target panel for the deeper analysis
+ * instead.
* The audit's F4 reproduction sat on 540 BP samples with all sub-
* windows reading 0 % because the seed data straddled the target
* ceiling; the tile keeps its place once even one sub-window
@@ -1096,9 +1097,10 @@ export default function DashboardPage() {
// small fractional float that the TrendCard's date-shaped
// formatter pipeline rendered as "1.1." — the regression the
// maintainer flagged in the post-v1.4.27 walk-through. The
- // all-time aggregate moves to the `/targets` BP card which
- // already shows the same number with more context; the
- // dashboard tile no longer needs to carry it. `avgAllTime`
+ // all-time aggregate moves to the Insights blood-pressure
+ // target panel which already shows the same number with more
+ // context; the dashboard tile no longer needs to carry it.
+ // `avgAllTime`
// also retires from the TrendCard API (this was its only
// consumer).
const bp7 = data?.bpInTargetPct7d ?? null;
diff --git a/src/app/targets/__tests__/page.test.tsx b/src/app/targets/__tests__/page.test.tsx
deleted file mode 100644
index 1fcbe607e..000000000
--- a/src/app/targets/__tests__/page.test.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import { describe, it, expect, vi } from "vitest";
-import { renderToStaticMarkup } from "react-dom/server";
-
-import { I18nProvider } from "@/lib/i18n/context";
-
-/**
- * v1.8.6 — `/targets` deprecation banner.
- *
- * The Targets (Zielwerte) page is deprecated: target-range editing moved
- * inline into the Insights category pages. The page stays routable but
- * carries a top banner telling the user where editing lives now and that
- * the page will be removed in a future release. This SSR test pins the
- * banner copy + structure.
- *
- * The page leans on `useAuth`, two `useQuery` reads, `useFeatureFlags`
- * and `useCoachHandoff`; all are stubbed so the render reaches the
- * banner without standing up an App-Router runtime or a QueryClient.
- */
-
-vi.mock("@/hooks/use-auth", () => ({
- useAuth: () => ({
- isAuthenticated: true,
- user: { id: "u1", disableCoach: false },
- }),
-}));
-
-// The page issues two `useQuery` reads (targets payload + provider
-// chain). Drive both off the same stub: the first call resolves the
-// targets payload, the rest return a null provider chain.
-let queryCall = 0;
-const TARGETS_PAYLOAD = {
- targets: [],
- bpDiastolic: { current: null, average30: null, range: null },
- profile: { heightCm: 175, age: 40, gender: "male", glucoseUnit: "mg/dL" },
-};
-vi.mock("@tanstack/react-query", () => ({
- useQuery: () => {
- queryCall += 1;
- if (queryCall === 1) {
- return { data: TARGETS_PAYLOAD, isLoading: false };
- }
- return { data: null, isLoading: false };
- },
-}));
-
-// Turn Coach off so the page never mounts the SSE drawer machinery.
-vi.mock("@/hooks/use-feature-flags", () => ({
- useFeatureFlags: () => ({
- enabled: false,
- coach: false,
- briefing: false,
- insightStatus: false,
- correlations: false,
- healthScoreExplainer: false,
- }),
-}));
-
-vi.mock("@/hooks/use-coach-handoff", () => ({
- useCoachHandoff: () => ({
- coachOpen: false,
- setCoachOpen: vi.fn(),
- coachPrefill: null,
- coachScope: null,
- askCoach: vi.fn(),
- }),
-}));
-
-const { default: TargetsPage } = await import("../page");
-
-function render(locale: "en" | "de" = "en") {
- queryCall = 0;
- return renderToStaticMarkup(
-
-
- ,
- );
-}
-
-describe("
deprecation banner", () => {
- it("renders the deprecation banner with title + body", () => {
- const html = render();
- expect(html).toContain('data-slot="targets-deprecation"');
- expect(html).toContain("This page is being retired");
- expect(html).toContain("Insights");
- // Reuses the warning-card visual pattern.
- expect(html).toContain("border-warning");
- });
-
- it("localises the banner copy", () => {
- const html = render("de");
- expect(html).toContain("Diese Seite wird eingestellt");
- });
-});
diff --git a/src/app/targets/page.tsx b/src/app/targets/page.tsx
deleted file mode 100644
index 20f8a80c2..000000000
--- a/src/app/targets/page.tsx
+++ /dev/null
@@ -1,313 +0,0 @@
-"use client";
-
-// DEPRECATED since v1.8.6. Target-range editing moved inline into the
-// Insights category pages (the per-metric reference panel now mounts the
-// same ), so this standalone page no longer appears in
-// the navigation. It stays routable behind the `/zielwerte`→`/targets`
-// proxy redirect and is a remove candidate after ~10 releases.
-//
-// What stays in place because Insights consumes the same API:
-// • GET /api/insights/targets — the read powering this page is the
-// same payload the Insights panel reads (`queryKeys.insightsTargets()`).
-// • PUT/DELETE /api/user/thresholds — the threshold override write the
-// inline calls.
-// • src/components/targets/* — TargetCard, TargetEditSheet, RangeBar,
-// ConsistencyStrip, TargetStatusPill, source-link helper are all
-// reused by the inline Insights panel.
-// Removing the page later is a UI-only change; no data or API moves.
-
-import { useMemo } from "react";
-import { useQuery } from "@tanstack/react-query";
-import { AlertCircle, Loader2 } from "lucide-react";
-
-import { useAuth } from "@/hooks/use-auth";
-import { useTranslations } from "@/lib/i18n/context";
-import { queryKeys } from "@/lib/query-keys";
-import { Card, CardContent } from "@/components/ui/card";
-import { convertGlucose, resolveGlucoseUnit } from "@/lib/glucose";
-import {
- TargetCard,
- type TargetCardData,
-} from "@/components/targets/target-card";
-import { TargetsSummaryHeader } from "@/components/targets/targets-summary-header";
-import { getTargetSourceLink } from "@/lib/targets/source-link";
-import { CoachDrawer } from "@/components/insights/coach-panel/coach-drawer";
-import { useCoachHandoff } from "@/hooks/use-coach-handoff";
-import { useFeatureFlags } from "@/hooks/use-feature-flags";
-
-type TargetData = TargetCardData;
-
-interface BpDiastolic {
- current: number | null;
- average30: number | null;
- range: { min: number; max: number } | null;
-}
-
-interface TargetPageSummary {
- targetsMetThisWeek: number;
- totalTargets: number;
- streakHighlight: { metric: string; days: number } | null;
-}
-
-interface TargetsResponse {
- targets: TargetData[];
- pageSummary?: TargetPageSummary;
- bpDiastolic: BpDiastolic;
- profile: {
- heightCm: number | null;
- age: number | null;
- gender: string | null;
- glucoseUnit?: string | null;
- };
-}
-
-/**
- * v1.4.25 W3e — provider-chain status used to gate the per-card Coach
- * CTA. Returns true when at least one provider is configured AND
- * enabled. The Settings → AI surface owns the same query (via
- * `queryKeys.insightsProviderChain()`) so the cache is shared.
- */
-interface ProviderChainStatus {
- activeProvider: string | null;
- cachedActiveProvider: string | null;
- configuredChain: Array<{
- providerType: string;
- enabled: boolean;
- available: boolean;
- }>;
-}
-
-const GLUCOSE_TYPES = new Set([
- "BLOOD_GLUCOSE_FASTING",
- "BLOOD_GLUCOSE_POSTPRANDIAL",
- "BLOOD_GLUCOSE_RANDOM",
- "BLOOD_GLUCOSE_BEDTIME",
-]);
-
-/**
- * Fixed card order per Marc directive — explicitly NOT status-sorted.
- * Marc wants a stable visual hierarchy so the user's eye lands on the
- * same metric in the same place every visit. Cards whose type is not
- * in this list (glucose contexts, future metrics) sort after the
- * core six in their server-emitted order.
- */
-const FIXED_TARGET_ORDER: Record = {
- BLOOD_PRESSURE: 0,
- BLOOD_PRESSURE_IN_TARGET: 1,
- WEIGHT: 2,
- PULSE: 3,
- BMI: 4,
- MOOD_SCORE: 5,
- MOOD_STABILITY: 6,
- MEDICATION_COMPLIANCE: 7,
- SLEEP_DURATION: 8,
- BODY_FAT: 9,
- ACTIVITY_STEPS: 10,
-};
-
-function sortKey(target: TargetData): number {
- return FIXED_TARGET_ORDER[target.type] ?? 100;
-}
-
-export default function TargetsPage() {
- const { t } = useTranslations();
- const { isAuthenticated, user } = useAuth();
-
- const { data, isLoading } = useQuery({
- queryKey: queryKeys.insightsTargets(),
- queryFn: async () => {
- const res = await fetch("/api/insights/targets");
- if (!res.ok) throw new Error(t("targets.loadError"));
- const json = await res.json();
- return json.data as TargetsResponse;
- },
- enabled: isAuthenticated,
- });
-
- // v1.4.25 W3e — share the cache with Settings → AI section. When the
- // user has no configured provider, `activeProvider` is null and the
- // per-card Coach CTA hides entirely (no broken-button state).
- const { data: chainStatus } = useQuery({
- queryKey: queryKeys.insightsProviderChain(),
- queryFn: async () => {
- const res = await fetch("/api/insights/provider-chain");
- if (!res.ok) return null;
- const json = await res.json();
- return json.data as ProviderChainStatus;
- },
- enabled: isAuthenticated,
- });
- // v1.4.37 W5 — operator's global Coach flag layered on top of the
- // per-user provider chain. Either gate disables every Coach
- // affordance on this page: the per-card CTAs (via `aiEnabled`
- // threaded into ) and the page-level mount
- // below. The flag default is all-on for fresh installs and on any
- // fetch error, so the path stays open by default.
- const flags = useFeatureFlags();
- // v1.4.47 W3 — per-user "Hide Coach" toggle is a peer gate to the
- // operator's master flag. When either is off, the per-card CTAs +
- // the page-level mount below both go dark.
- const coachEnabled = flags.coach && !user?.disableCoach;
- const aiEnabled = coachEnabled && chainStatus?.activeProvider != null;
-
- // v1.4.25 W3e — Coach drawer state owned by the page. The per-card
- // CTA + summary header both feed the same drawer so the user only
- // ever sees one drawer instance.
- const { coachOpen, setCoachOpen, coachPrefill, coachScope, askCoach } =
- useCoachHandoff();
-
- const visibleTargets = useMemo(() => {
- if (!data) return [] as TargetData[];
- const displayGlucoseUnit = resolveGlucoseUnit(
- data.profile.glucoseUnit ?? null,
- );
- return data.targets
- .filter((target) => target.current != null)
- .map((target) => {
- if (!GLUCOSE_TYPES.has(target.type)) return target;
- // Server label is an i18n key for glucose; resolve here.
- const label = t(target.label);
- // Convert mg/dL canonical values to the user's display unit.
- const convert = (v: number | null) =>
- v == null ? null : convertGlucose(v, displayGlucoseUnit);
- return {
- ...target,
- label,
- unit: displayGlucoseUnit,
- current: convert(target.current),
- average30: convert(target.average30),
- range: target.range
- ? {
- min: convertGlucose(target.range.min, displayGlucoseUnit),
- max: convertGlucose(target.range.max, displayGlucoseUnit),
- }
- : null,
- };
- })
- .sort((a, b) => sortKey(a) - sortKey(b));
- }, [data, t]);
-
- if (isLoading) {
- return (
-
-
-
- );
- }
-
- if (!data) {
- return (
-
- {t("common.noData")}
-
- );
- }
-
- const profileIncomplete = !data.profile.heightCm || !data.profile.age;
-
- return (
- // v1.4.19 phase A7 — the maintainer reported "relativ viel Platz" wasted
- // between the overview header and the first values on `/targets`.
- // Tighten the rhythm from `space-y-8` (32 px) to `space-y-6`
- // (24 px) — that matches the admin / settings pages.
-
-
-
{t("targets.title")}
-
{t("targets.introText")}
-
-
- {/* v1.8.6 — single warning banner. Target editing moved inline into
- the Insights category pages; this page stays reachable but will
- be removed in a future release.
-
- Design Low fix: the deprecation notice and the profile-incomplete
- hint used to stack as two near-identical warning cards. They are
- folded into one card now — the deprecation message stays the
- headline, and the profile-incomplete detail (height / age missing)
- renders as a secondary line beneath it only when relevant, so the
- page never shows two warning banners. */}
-
-
-
-
-
-
- {t("targets.deprecation.title")}
-
-
- {t("targets.deprecation.body")}
-
-
- {profileIncomplete && (
-
-
- {t("targets.profileIncomplete")}
-
-
- {!data.profile.heightCm && !data.profile.age
- ? t("targets.profileIncompleteHeightAge")
- : !data.profile.heightCm
- ? t("targets.profileIncompleteHeight")
- : t("targets.profileIncompleteAge")}
-
-
- )}
-
-
-
-
- {/* v1.4.25 W3e — page-level summary line. Renders nothing when
- the API hasn't shipped pageSummary yet (older clients during
- rollout / mocked test fixtures that pre-date this addition). */}
- {data.pageSummary && (
-
- )}
-
- {/* v1.4.25 W3e — responsive grid. Mobile (default): single
- column. sm (640px+): two columns. lg (1024px+): three columns,
- matching the dashboard / insights rhythm. Cards reflow
- internally too — see
. */}
- {visibleTargets.length > 0 ? (
-
- {visibleTargets.map((target) => (
-
- ))}
-
- ) : (
-
- {t("targets.noMeasurementData")}
-
- )}
-
- {/* v1.4.25 W3e — Coach drawer mounted at the page level. The
- drawer is fully-controlled; per-card CTAs flip `coachOpen`
- and seed `coachPrefill` via the `askCoach()` hook callback.
- v1.4.37 W5 — short-circuit when the operator turned the
- global Coach flag off so the SSE machinery + Sheet portal
- never mount. The per-card CTAs already hide via `aiEnabled`
- above, so the drawer can never be triggered with the flag
- off; the gate is defence-in-depth. */}
- {coachEnabled && (
-
- )}
-
- );
-}
diff --git a/src/components/charts/trend-card.tsx b/src/components/charts/trend-card.tsx
index 8addfbfb6..e69183791 100644
--- a/src/components/charts/trend-card.tsx
+++ b/src/components/charts/trend-card.tsx
@@ -423,8 +423,8 @@ export function TrendCard({
{/* v1.4.28 FB-C2 — the all-time third sub-value retired
alongside the `avgAllTime*` props. The BD-Zielbereich
tile was the only consumer; the same number stays
- visible on the `/targets` Blutdruck card with full
- context. Every dashboard tile now ships the same two
+ visible on the Insights blood-pressure target panel with
+ full context. Every dashboard tile now ships the same two
sub-rows. */}
diff --git a/src/components/insights/metric-target-summary.tsx b/src/components/insights/metric-target-summary.tsx
index b875518c1..d46cc012e 100644
--- a/src/components/insights/metric-target-summary.tsx
+++ b/src/components/insights/metric-target-summary.tsx
@@ -18,18 +18,16 @@ import { getTargetSourceLink } from "@/lib/targets/source-link";
* v1.8.0 → v1.8.5 — surface the per-metric target reference panel on each
* insights category page, directly beneath the chart / assessment.
*
- * The data is the same `/api/insights/targets` payload that powers the
- * `/targets` editing surface — this component reads the shared
- * `queryKeys.insightsTargets()` cache, so on a session that has already
- * visited `/targets` the read is free, and on a cold sub-page it warms a
- * cache the `/targets` page then reuses. We never recompute the ranges
- * here: the route is the single source of truth for the age-based ESH
- * blood-pressure band, the WHO weight / BMI band, the Karvonen pulse
- * band, the AASM sleep band, the ADA / DDG glucose bands, and the
- * medication / mood targets.
+ * The data is the `/api/insights/targets` payload, read through the
+ * shared `queryKeys.insightsTargets()` cache so warm sessions pay
+ * nothing and a cold sub-page warms the cache its siblings reuse. We
+ * never recompute the ranges here: the route is the single source of
+ * truth for the age-based ESH blood-pressure band, the WHO weight / BMI
+ * band, the Karvonen pulse band, the AASM sleep band, the ADA / DDG
+ * glucose bands, and the medication / mood targets.
*
- * v1.8.5 W5 — the panel grew from a one-line range card into a compact
- * reference panel that ports the context the `/targets` card carries:
+ * The panel is a compact reference panel carrying the full target
+ * context:
*
* • Row 1: target range string + `