- {t("charts.avg7dShort")}:{" "}
+ {t(avg7LabelKey)}:{" "}
{avg7Hint ? (
@@ -159,6 +202,15 @@ export function TrendCard({
{avg7 !== null ? renderPair(avg7, secondary?.avg7) : "—"}
)}
+ {trend7Delta != null && (
+
+ ({formatDelta(trend7Delta)})
+
+ )}
{t("charts.avg30dShort")}:{" "}
diff --git a/src/lib/analytics/__tests__/trend-delta.test.ts b/src/lib/analytics/__tests__/trend-delta.test.ts
new file mode 100644
index 000000000..fb2320649
--- /dev/null
+++ b/src/lib/analytics/__tests__/trend-delta.test.ts
@@ -0,0 +1,39 @@
+import { describe, it, expect } from "vitest";
+import { summaryToTrend7Delta } from "../trend-delta";
+
+const STABLE = { slope: 0, direction: "stable" as const, confidence: 0.1 };
+const RISING_HALF_PER_DAY = {
+ slope: 0.5,
+ direction: "up" as const,
+ confidence: 0.8,
+};
+const FALLING_TENTH_PER_DAY = {
+ slope: -0.1,
+ direction: "down" as const,
+ confidence: 0.7,
+};
+
+describe("summaryToTrend7Delta()", () => {
+ it("returns null when the summary is missing", () => {
+ expect(summaryToTrend7Delta(null)).toBeNull();
+ expect(summaryToTrend7Delta(undefined)).toBeNull();
+ });
+
+ it("returns null when slope7 is not computable (insufficient data)", () => {
+ expect(summaryToTrend7Delta({ slope7: null })).toBeNull();
+ });
+
+ it("projects slope (units per day) over a 7-day window", () => {
+ expect(summaryToTrend7Delta({ slope7: RISING_HALF_PER_DAY })).toBe(3.5);
+ // Negative slope produces a negative delta — the tile shows it
+ // with the unicode minus sign in the formatter.
+ expect(summaryToTrend7Delta({ slope7: FALLING_TENTH_PER_DAY })).toBeCloseTo(
+ -0.7,
+ 3,
+ );
+ });
+
+ it("returns 0 for a stable slope (the tile then paints ±0)", () => {
+ expect(summaryToTrend7Delta({ slope7: STABLE })).toBe(0);
+ });
+});
diff --git a/src/lib/analytics/trend-delta.ts b/src/lib/analytics/trend-delta.ts
new file mode 100644
index 000000000..8d682a294
--- /dev/null
+++ b/src/lib/analytics/trend-delta.ts
@@ -0,0 +1,35 @@
+/**
+ * Compute a 7-day trend delta from a metric summary.
+ *
+ * Backstory — v1.4.15 Fix 4 ("7-Tage-Schnitt" → "7-Tage-Trend"):
+ * the dashboard tiles labelled the 7-day average as "7T:" / "7d:"
+ * with the mean value beside it. Marc found that label unclear
+ * (sounded like an "average") when the goal of the tile is to
+ * convey *direction* over the past week. We re-use the existing
+ * 7-day linear-regression slope (already in `DataSummary.slope7`)
+ * and project a stable end-to-end delta as
+ *
+ * delta = slope.slope * 7 // units per day → unit delta over 7d
+ *
+ * The trend-card then paints the delta with metric-aware sentiment
+ * colour (up-good vs up-bad). Returning `null` when slope7 is not
+ * available keeps the tile rendering the legacy avg label for back-
+ * compat with call sites that haven't been migrated.
+ *
+ * Pure & deterministic — pinned by unit test.
+ */
+import type { TrendSlope } from "@/lib/analytics/trends";
+
+export interface TrendCapableSummary {
+ slope7: TrendSlope | null;
+}
+
+export function summaryToTrend7Delta(
+ summary: TrendCapableSummary | undefined | null,
+): number | null {
+ const slope7 = summary?.slope7;
+ if (!slope7) return null;
+ // slope.slope is units per day (see lib/analytics/trends.ts) → 7 days
+ // is the projected end-to-end delta of the regression line.
+ return slope7.slope * 7;
+}