diff --git a/src/app/insights/weight/page.tsx b/src/app/insights/weight/page.tsx
index b621a2600..453a2e3e4 100644
--- a/src/app/insights/weight/page.tsx
+++ b/src/app/insights/weight/page.tsx
@@ -18,7 +18,6 @@ import { MetricCorrelationCard } from "@/components/insights/metric-correlation-
import { MeasurementDiversityNudge } from "@/components/insights/measurement-diversity-nudge";
import { MetricTargetSummary } from "@/components/insights/metric-target-summary";
import { SubPageShell } from "@/components/insights/sub-page-shell";
-import { TrajectoryForecastCard } from "@/components/insights/derived/trajectory-forecast-card";
import { buildWeightBandsFromHeight } from "@/lib/analytics/value-bands";
/**
@@ -115,14 +114,6 @@ export default function InsightsGewichtPage() {
-
-
{/* v1.12.0 — Weight owns the weight × weekday correlation (relocated
off the overview onto its metric page). */}
diff --git a/src/components/insights/derived/trajectory-fan.tsx b/src/components/insights/derived/trajectory-fan.tsx
deleted file mode 100644
index d3f8b11ad..000000000
--- a/src/components/insights/derived/trajectory-fan.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-"use client";
-
-import {
- ResponsiveContainer,
- ComposedChart,
- Area,
- Line,
- XAxis,
- YAxis,
- ReferenceLine,
-} from "recharts";
-
-/**
- * v1.12.1 — the Recharts fan band of the trajectory forecast card,
- * split into its own module so it can be `next/dynamic`-deferred.
- *
- * `TrajectoryForecastCard` is mounted by the shared `HealthKitMetricPage`
- * on every trajectory-eligible metric page; before this split it pulled
- * Recharts into the page's initial chunk eagerly (the main metric chart
- * is already deferred via `HealthChartDynamic`). Isolating the only
- * Recharts-touching subtree here lets the card chrome render without the
- * chart library and lazy-load this chunk only when there's a projection
- * to draw — keeping the chart visually identical (same ComposedChart /
- * Area / Line / ReferenceLine geometry, same `isAnimationActive={false}`).
- */
-
-export interface TrajectoryFanPoint {
- date: string;
- projected: number;
- /** Stacked transparent base (= bandLow). */
- base: number;
- /** Shaded band height (= bandHigh − bandLow). */
- range: number;
-}
-
-export function TrajectoryFan({
- points,
- color,
-}: {
- points: TrajectoryFanPoint[];
- color: string;
-}) {
- return (
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/src/components/insights/derived/trajectory-forecast-card.tsx b/src/components/insights/derived/trajectory-forecast-card.tsx
deleted file mode 100644
index d9cc97903..000000000
--- a/src/components/insights/derived/trajectory-forecast-card.tsx
+++ /dev/null
@@ -1,331 +0,0 @@
-"use client";
-
-import { useMemo } from "react";
-import dynamic from "next/dynamic";
-import { TrendingUp, TrendingDown, Minus } from "lucide-react";
-
-import { useTranslations } from "@/lib/i18n/context";
-import { cn } from "@/lib/utils";
-import { LearningGate } from "@/components/ui/learning-gate";
-import { CoverageMeter } from "@/components/insights/derived/coverage-meter";
-import { ProvenanceExplainer } from "@/components/insights/derived/provenance-explainer";
-import { METRIC_PROVENANCE } from "@/components/insights/derived/standards";
-import { useDerivedMetric } from "@/components/insights/derived/use-derived-metric";
-
-// v1.12.1 — defer the Recharts fan band so the card chrome (and the whole
-// HealthKit metric page that always mounts this card on a trajectory-
-// eligible type) no longer pulls Recharts into the initial chunk. The
-// loading shell is a plain box that fills the same fixed-height band the
-// chart paints, so there is no layout shift when the chunk arrives.
-const TrajectoryFanDynamic = dynamic(
- () =>
- import("@/components/insights/derived/trajectory-fan").then((mod) => ({
- default: mod.TrajectoryFan,
- })),
- {
- ssr: false,
- loading: () =>
,
- },
-);
-// Type-only — the compute payload never drags the server graph into the
-// client bundle (the v1.9.0 lesson, mirrored across the derived surfaces).
-import type { TrajectoryValue } from "@/lib/insights/derived/trajectory";
-import type { DerivedProvenance } from "@/lib/insights/derived/types";
-
-/**
- * v1.11.0 (Epic B, Pillar 3) — the short-horizon projection card on the
- * metric detail page.
- *
- * Surfaces the deterministic `TRAJECTORY` derived metric as a small band
- * chart: the recent observed line, then the projected line (dashed) with a
- * widening prediction-interval fan. The fan VISIBLY widens with the horizon
- * — that is the uncertainty communication, no words needed. The number is
- * computed server-side; this card only draws it. It is explicitly labelled
- * a PROJECTION ("if this pattern continues"), never a prediction or a dated
- * event, and the cited method (OLS + prediction interval) rides the ⓘ
- * provenance affordance.
- *
- * Below the engine's R² / history / staleness floor the engine returns
- * `insufficient`, and the card renders the same calm "not enough of a trend
- * to project yet" + coverage state every derived surface uses — never a weak
- * line, never a blank.
- *
- * The card mounts as a sibling beneath the main metric chart (mirroring
- * `MetricStatusCard`), so the main chart's visual identity is untouched.
- */
-
-const TRAJECTORY_METRIC = "TRAJECTORY";
-
-/**
- * Mirror of `TRAJECTORY_MIN_HISTORY_DAYS` (`src/lib/insights/derived/trajectory.ts`).
- * Re-declared as a client-side literal rather than value-imported because the
- * engine module pulls the rollup/baseline server graph in at its top level —
- * importing the constant would drag that graph into the client bundle (the same
- * reason `TrajectoryValue` is a type-only import). Keep this in lockstep with
- * the engine; the unit test pins both at 14.
- */
-const TRAJECTORY_MIN_HISTORY_DAYS = 14;
-
-/** A single value-scale used to render stored values in the display unit. */
-function scaled(value: number, scale: number): number {
- return value * scale;
-}
-
-interface TrajectoryForecastCardProps {
- /** The MeasurementType to project (the page's canonical type). */
- type: string;
- /** Display unit suffix (e.g. "kg"). */
- unit?: string;
- /** Display scale folded into every rendered value (defaults to 1). */
- valueScale?: number;
- /** Canonical chart colour for the metric's line. */
- color?: string;
- /** Gate the underlying derived read (e.g. on the auth/empty flag). */
- enabled?: boolean;
- /**
- * v1.12.0 — density by render context. Sub-pages pass `compact` so the
- * card sheds the `min-h-48` floor + drops to a shorter chart and a tighter
- * no-trend state (a too-tall "1 of 1" empty band was the maintainer flag).
- * The Insights OVERVIEW grid omits it so the tile KEEPS its current size
- * for grid symmetry — the default is the unchanged, roomier geometry.
- */
- compact?: boolean;
- className?: string;
-}
-
-/** The provenance ⓘ explainer for the projection, wired from the map. */
-function TrajectoryProvenance({
- provenance,
-}: {
- provenance: DerivedProvenance;
-}) {
- const { t } = useTranslations();
- const meta = METRIC_PROVENANCE.TRAJECTORY;
- const method = (
- <>
- {meta.caveatKey ? (
-
- {t(meta.caveatKey)}
-
- ) : null}
- {t(meta.methodKey)}
- >
- );
- return (
-
- );
-}
-
-/** Card shell — uppercase label + provenance affordance + body children. */
-function CardShell({
- provenance,
- compact = false,
- children,
-}: {
- provenance?: DerivedProvenance;
- compact?: boolean;
- children: React.ReactNode;
-}) {
- const { t } = useTranslations();
- return (
-
-
-
- {t("insights.derived.trajectory.cardTitle")}
-
- {provenance ? : null}
-
- {children}
-
- );
-}
-
-/** CLS-safe skeleton matching the resolved card geometry. */
-function CardSkeleton({ compact = false }: { compact?: boolean }) {
- return (
-
- );
-}
-
-export function TrajectoryForecastCard({
- type,
- unit,
- valueScale = 1,
- color = "var(--chart-1)",
- enabled = true,
- compact = false,
- className,
-}: TrajectoryForecastCardProps) {
- const { t } = useTranslations();
- const { data } = useDerivedMetric(TRAJECTORY_METRIC, {
- type,
- enabled,
- });
-
- // Assemble the band chart series: the projected fan only (the engine
- // returns the projection points; the recent observed value anchors it as
- // the dashed line's left edge). Each projection point carries the band
- // `low`/`base` (= bandLow) + `range` (= bandHigh − bandLow) so a stacked
- // Area renders the fan, plus the projected line.
- const chart = useMemo(() => {
- if (!data || data.status !== "ok" || !data.value) return null;
- const v = data.value;
- const points = v.projection.map((p) => ({
- date: p.date,
- projected: scaled(p.projected, valueScale),
- base: scaled(p.bandLow, valueScale),
- range: scaled(p.bandHigh - p.bandLow, valueScale),
- }));
- // Anchor the fan at the last observed value so the dashed line departs
- // from "today" rather than floating.
- const anchor = {
- date: "now",
- projected: scaled(v.lastValue, valueScale),
- base: scaled(v.lastValue, valueScale),
- range: 0,
- };
- return {
- points: [anchor, ...points],
- lastValue: scaled(v.lastValue, valueScale),
- };
- }, [data, valueScale]);
-
- if (!data) {
- return (
-
-
-
- );
- }
-
- // Below the fit / history / staleness floor — calm "no trend to project"
- // state + coverage, never a weak line.
- if (data.status === "insufficient" || !chart) {
- // v1.12.0 — surface how much history is still missing before a projection
- // can appear, drawn from the same coverage envelope the meter reads. The
- // gating floor is distinct-in-window days, so the hint only shows when the
- // shortfall is a history-depth one (the dominant insufficient cause); when
- // the shortfall is fit quality (enough days, weak R²) the days are already
- // there and the generic copy stands on its own.
- const daysNeeded = Math.max(
- 0,
- TRAJECTORY_MIN_HISTORY_DAYS - Math.floor(data.coverage.historyDays),
- );
- return (
-
-
- 0
- ? t("insights.derived.trajectory.daysNeeded", {
- count: daysNeeded,
- })
- : undefined
- }
- />
-
-
-
- );
- }
-
- const v = data.value!;
- const DirectionIcon =
- v.direction === "up"
- ? TrendingUp
- : v.direction === "down"
- ? TrendingDown
- : Minus;
- const last = chart.points[chart.points.length - 1];
- const projectedLow = last.base;
- const projectedHigh = last.base + last.range;
-
- // The headline is conditional by construction — "if this pattern
- // continues" — and quotes the RANGE, never a single certain number.
- const rangeText = t("insights.derived.trajectory.range", {
- low: Math.round(projectedLow * 10) / 10,
- high: Math.round(projectedHigh * 10) / 10,
- unit: unit ?? "",
- days: v.horizonDays,
- });
-
- return (
-
-
-
-
-
-
- {t("insights.derived.trajectory.headline", {
- days: v.horizonDays,
- })}
-
-
- {rangeText}
-
-
-
-
- {/* The fan: a stacked transparent base + a shaded range Area, then
- the dashed projected line. The shaded band visibly widens toward
- the horizon — the uncertainty signal. */}
-
-
-
-
- {/* The mandatory conditional caveat — never a certainty, never a
- dated clinical event. */}
-
- {t("insights.derived.trajectory.caveat")}
-
-
-
- );
-}
diff --git a/src/components/insights/healthkit-metric-page.tsx b/src/components/insights/healthkit-metric-page.tsx
index cb3bc8582..8fa944a1d 100644
--- a/src/components/insights/healthkit-metric-page.tsx
+++ b/src/components/insights/healthkit-metric-page.tsx
@@ -19,8 +19,6 @@ import { Skeleton } from "@/components/ui/skeleton";
import { ChartSkeleton } from "@/components/charts/chart-skeleton";
import { HealthChartDynamic } from "@/components/charts/health-chart-dynamic";
import { MetricStatusCard } from "@/components/insights/metric-status-card";
-import { TrajectoryForecastCard } from "@/components/insights/derived/trajectory-forecast-card";
-import { isTrajectoryType } from "@/lib/insights/derived/registry";
import { MetricEmptyState } from "@/components/insights/metric-empty-state";
import { MetricStatStrip } from "@/components/insights/metric-stat-strip";
import { CoachReadStrip } from "@/components/insights/derived/coach-read-strip";
@@ -150,17 +148,6 @@ export interface HealthKitMetricPageProps {
* is a compile error, not a silent 422.
*/
statusMetric?: MetricStatusMetricId;
- /**
- * v1.11.0 (Epic B, Pillar 3) — when `true`, mounts the short-horizon
- * `` beneath the chart for this metric (the page's
- * `measurementType`). Opt-in: a page sets it only for a slow daily
- * physiological series where a conservative 7–14-day OLS projection reads
- * honestly. The card is only rendered on the data-bearing branch and only
- * when the type is a supported trajectory metric; the engine still gates
- * on R² / history / staleness, so a flat or noisy series shows the calm
- * "no trend to project yet" state rather than a line.
- */
- forecast?: boolean;
/**
* v1.16.16 — decimal precision for the stat-strip values. Defaults to the
* strip's own default (1). Blood glucose passes 0 for mg/dL (integer
@@ -203,7 +190,6 @@ export function HealthKitMetricPage({
targetSummarySlug,
statusMetric,
statIcon,
- forecast = false,
statFractionDigits,
statMedianLabel,
afterChart,
@@ -397,16 +383,6 @@ export function HealthKitMetricPage({
{/* v1.17.0 — page-specific extra block (blood glucose: the clinical
panel). Mounted on the data-bearing branch only. */}
{afterChart}
- {forecast && isTrajectoryType(measurementType) ? (
-
- ) : null}
{/* v1.12.0 — Einschätzung is the last block on the canonical
metric-detail spine. */}
{statusMetric ? (