diff --git a/messages/de.json b/messages/de.json index 851fc70fc..8185072d0 100644 --- a/messages/de.json +++ b/messages/de.json @@ -827,7 +827,6 @@ "movingAverage7d": "7-Tage-Trend", "avg7dShort": "7T", "avg30dShort": "30T", - "avgAllTimeShort": "Allzeit", "trend7dShort": "7-Tage-Trend", "trend": "Trend", "perWeek": "/Woche", diff --git a/messages/en.json b/messages/en.json index e99edae8d..2309fd122 100644 --- a/messages/en.json +++ b/messages/en.json @@ -827,7 +827,6 @@ "movingAverage7d": "7-day trend", "avg7dShort": "7d", "avg30dShort": "30d", - "avgAllTimeShort": "All-time", "trend7dShort": "7-day trend", "trend": "Trend", "perWeek": "/week", diff --git a/messages/es.json b/messages/es.json index 6f0c48098..16beddbed 100644 --- a/messages/es.json +++ b/messages/es.json @@ -827,7 +827,6 @@ "movingAverage7d": "7-day trend", "avg7dShort": "7d", "avg30dShort": "30d", - "avgAllTimeShort": "All-time", "trend7dShort": "7-day trend", "trend": "Tendencia", "perWeek": "/week", diff --git a/messages/fr.json b/messages/fr.json index f666f89fc..330182c11 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -827,7 +827,6 @@ "movingAverage7d": "7-day trend", "avg7dShort": "7d", "avg30dShort": "30d", - "avgAllTimeShort": "All-time", "trend7dShort": "7-day trend", "trend": "Tendance", "perWeek": "/week", diff --git a/messages/it.json b/messages/it.json index 0eadb7161..bd3915645 100644 --- a/messages/it.json +++ b/messages/it.json @@ -827,7 +827,6 @@ "movingAverage7d": "7-day trend", "avg7dShort": "7d", "avg30dShort": "30d", - "avgAllTimeShort": "All-time", "trend7dShort": "7-day trend", "trend": "Tendenza", "perWeek": "/week", diff --git a/messages/pl.json b/messages/pl.json index b7541105f..46c9d3f9d 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -827,7 +827,6 @@ "movingAverage7d": "7-day trend", "avg7dShort": "7d", "avg30dShort": "30d", - "avgAllTimeShort": "All-time", "trend7dShort": "7-day trend", "trend": "Trend", "perWeek": "/week", diff --git a/src/app/page.tsx b/src/app/page.tsx index fdfa08f53..420be6249 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -852,37 +852,24 @@ export default function DashboardPage() { tile (no DataSummary) so the prior-period delta uses `bpInTargetPctAllTime` as the long-arc baseline — when comparison is off the field stays null. */ + // v1.4.28 FB-C1 + FB-C2 — rewrite the BD-Zielbereich tile + // against the shared `` primitive so it matches + // the Weight / BP / Pulse sibling tiles exactly. The + // synthetic `bpSlope30 = bpTrendDelta / 30` block produced a + // small float (≈ 1.1 for a 33-point delta) that the + // TrendCard's date-shaped formatter pipeline rendered as + // "1.1." — the maintainer's screenshot regression. 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` + // also retires from the TrendCard API (this was its only + // consumer). const bp7 = data?.bpInTargetPct7d ?? null; const bp30 = data?.bpInTargetPct30d ?? null; - const bpAll = data?.bpInTargetPctAllTime ?? null; const bpPriorMonth = data?.bpInTargetPctPriorMonth ?? null; const bpPriorYear = data?.bpInTargetPctPriorYear ?? null; const bpTrendDelta = bp7 !== null && bp30 !== null ? bp7 - bp30 : null; - const bpSlope30: import("@/lib/analytics/trends").TrendSlope | null = - bpTrendDelta === null - ? null - : { - slope: bpTrendDelta / 30, - direction: - bpTrendDelta > 0.5 - ? "up" - : bpTrendDelta < -0.5 - ? "down" - : "stable", - // Synthetic slope (last-7d minus last-30d) carries no - // R² — pin to 1 so existing TrendSlope consumers don't - // mis-interpret a `0` as "very low confidence". - confidence: 1, - }; - // v1.4.22 W5 reconcile (Code-H2) — match the comparison - // window the user picked. `lastMonth` baseline ⇒ compare - // last30 against the now-60d…now-30d window (priorMonth); - // `lastYear` baseline ⇒ compare against the matching window - // shifted back 365 days. The previous shortcut subtracted - // `bpAll` regardless of baseline, which produced a number - // whose magnitude was honest but whose label ("vs. last - // month") lied to the user. const bpComparePrior = compareBaseline === "lastMonth" ? bpPriorMonth @@ -904,14 +891,9 @@ export default function DashboardPage() { label={t("dashboard.bpInTargetShort")} latest={data?.bpInTargetPct ?? null} unit="%" - /* v1.4.18 A1 — wire 7T / 30T sub-values from the new - windowed analytics fields. Up to v1.4.17 these were - hard-coded to null and rendered "—" even when the - user had paired BP readings in both windows. */ avg7={bp7} avg30={bp30} - avgAllTime={bpAll} - slope30={bpSlope30} + slope30={null} trend7Delta={bpTrendDelta} icon={Target} directionSentiment="up-good" diff --git a/src/components/charts/__tests__/bd-tile.test.tsx b/src/components/charts/__tests__/bd-tile.test.tsx new file mode 100644 index 000000000..1adf2e09d --- /dev/null +++ b/src/components/charts/__tests__/bd-tile.test.tsx @@ -0,0 +1,117 @@ +import { describe, it, expect } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; +import { Target, Activity } from "lucide-react"; + +import { TrendCard } from "../trend-card"; +import { I18nProvider } from "@/lib/i18n/context"; + +/** + * v1.4.28 R3a FB-C1 + FB-C2 — BD-Zielbereich tile shape parity. + * + * Until v1.4.27 the dashboard mounted the BD tile through a special + * branch that: + * 1. Synthesised a fake `slope30 = bpTrendDelta / 30` so the arrow + * could move (the result was a small float like 1.1 that the + * TrendCard's downstream formatter pipeline could render as + * "1.1." — the maintainer's screenshot regression). + * 2. Passed `avgAllTime` so a third sub-row showed the all-time + * percentage — the BD tile was the only consumer of that prop. + * + * Both customisations retire in v1.4.28: the BD tile now mounts + * through the same TrendCard contract as Weight / BP / Pulse and the + * `avgAllTime*` prop family drops entirely. This spec is the + * regression lock against the divergent shape returning. + */ + +function render(node: React.ReactNode) { + return renderToStaticMarkup( + {node}, + ); +} + +describe("BD-Zielbereich tile (v1.4.28 FB-C1/C2 rewrite)", () => { + it("renders a numeric percent headline, never a date-shaped literal", () => { + const html = render( + , + ); + // The headline value-row must include "%" and an integer/percent + // string — never a `d.d.` artefact from the legacy synthetic-slope + // formatter chain. + expect(html).toContain('data-slot="trend-card-value-row"'); + const valueRowMatch = html.match( + /data-slot="trend-card-value-row"[^>]*>([\s\S]*?)<\/div>/, + ); + expect(valueRowMatch).not.toBeNull(); + const valueText = valueRowMatch![1]; + expect(valueText).toContain("%"); + expect(valueText).not.toMatch(/\d{1,2}\.\d{1,2}\. { + const bdHtml = render( + , + ); + const weightHtml = render( + , + ); + + // Same primitive slots on both tiles. + for (const slot of [ + "trend-card-value-row", + ]) { + expect(bdHtml).toContain(`data-slot="${slot}"`); + expect(weightHtml).toContain(`data-slot="${slot}"`); + } + + // The retired all-time slot must NOT render on either tile. + expect(bdHtml).not.toContain('data-slot="trend-card-all-time"'); + expect(weightHtml).not.toContain('data-slot="trend-card-all-time"'); + }); + + it("renders a dash headline when the metric has no data yet", () => { + const html = render( + , + ); + expect(html).toContain('data-slot="trend-card-value-row"'); + expect(html).toContain("—"); + }); +}); diff --git a/src/components/charts/__tests__/trend-card-all-time.test.tsx b/src/components/charts/__tests__/trend-card-all-time.test.tsx deleted file mode 100644 index 5362d714c..000000000 --- a/src/components/charts/__tests__/trend-card-all-time.test.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { renderToStaticMarkup } from "react-dom/server"; -import { Target } from "lucide-react"; - -import { TrendCard } from "../trend-card"; -import { I18nProvider } from "@/lib/i18n/context"; - -/** - * v1.4.22 A2 — BD-Zielbereich tile feature parity. - * - * The BP-in-target tile now ships the same set of features every other - * tile already has: - * - the optional `avgAllTime` sub-value renders next to `7d` and - * `30d` so the long-arc number stays visible after v1.4.22 A1 - * re-anchored the headline to the last-30-day window. - * - the trend arrow + 7-day-trend chip use a synthesised slope from - * the difference between the 7-day and 30-day in-target shares. - * - the comparison overlay flows through the same `compareBaseline` - * / `compareDelta` props the dashboard wires for every other tile. - * - * Other tiles leave `avgAllTime` undefined so the third sub-row never - * renders — pinned via the negative case at the bottom. - */ - -function render(node: React.ReactNode) { - return renderToStaticMarkup( - {node}, - ); -} - -const baseProps = { - label: "BP in target", - latest: 50, - unit: "%", - avg7: 55, - avg30: 50, - slope30: null, - icon: Target, - directionSentiment: "up-good" as const, -}; - -describe(" avgAllTime sub-value (v1.4.22 A2)", () => { - it("renders the all-time label + value when `avgAllTime` is provided", () => { - const html = render(); - // The slot is keyed via `data-slot` so callers can target it - // without scraping translated copy. - expect(html).toContain('data-slot="trend-card-all-time"'); - expect(html).toContain("All-time"); - // The headline (50 %) is the 30-day pct after v1.4.22 A1; the - // all-time number lives in the third sub-row. Both must render - // (the formatter emits a trailing `.0` for integer values via the - // shared `useFormatters().number(_, 1)` contract — same shape as - // every other tile uses). - expect(html).toContain(">50.0<"); - expect(html).toContain(">11.0<"); - }); - - it("paints the all-time value with the supplied color class", () => { - const html = render( - , - ); - expect(html).toContain("text-dracula-orange"); - }); - - it("renders a dash for a null `avgAllTime` (no data yet) instead of a literal 'null'", () => { - const html = render(); - expect(html).toContain('data-slot="trend-card-all-time"'); - expect(html).toContain(">—<"); - expect(html).not.toContain("null"); - }); - - it("does NOT render the all-time slot when `avgAllTime` is undefined (other tiles untouched)", () => { - const html = render(); - expect(html).not.toContain('data-slot="trend-card-all-time"'); - }); - - it("ships the trend arrow when a synthetic slope is provided", () => { - // Synthetic slope: 7d (55) above 30d (50) → up direction → green - // arrow under the up-good sentiment. - const html = render( - , - ); - expect(html).toContain("text-dracula-green"); - }); - - it("ships the 7-day-trend chip when a delta is provided", () => { - const html = render( - , - ); - // The TrendCard renders the delta as e.g. `(+5.0)` next to `7d:`. - expect(html).toMatch(/\(\+5\.0\)/); - }); - - it("ships the comparison overlay when comparison is active", () => { - const html = render( - , - ); - expect(html).toContain('data-slot="tile-compare-delta"'); - expect(html).toContain("+4.0"); - }); -}); diff --git a/src/components/charts/__tests__/trend-card.test.tsx b/src/components/charts/__tests__/trend-card.test.tsx index ab0915d24..a16977ba6 100644 --- a/src/components/charts/__tests__/trend-card.test.tsx +++ b/src/components/charts/__tests__/trend-card.test.tsx @@ -125,6 +125,9 @@ describe(" directionSentiment", () => { describe(" responsive layout", () => { it("keeps long BP target tile content wrappable inside the card", () => { + // v1.4.28 FB-C2 — the BD-Zielbereich tile dropped `avgAllTime` + // and the synthetic slope; assert the surviving shape (overflow + // wrap discipline, compare-delta callout) still ships. const html = render( responsive layout", () => { unit="%" avg7={100} avg30={50} - avgAllTime={11} - slope30={RISING} + slope30={null} trend7Delta={50} icon={Activity} directionSentiment="up-good" @@ -145,6 +147,6 @@ describe(" responsive layout", () => { expect(html).toContain("min-w-0"); expect(html).toContain("[overflow-wrap:anywhere]"); expect(html).toContain('data-slot="tile-compare-delta"'); - expect(html).toContain('data-slot="trend-card-all-time"'); + expect(html).not.toContain('data-slot="trend-card-all-time"'); }); }); diff --git a/src/components/charts/trend-card.tsx b/src/components/charts/trend-card.tsx index 8756ae486..6e1512aef 100644 --- a/src/components/charts/trend-card.tsx +++ b/src/components/charts/trend-card.tsx @@ -87,17 +87,6 @@ interface TrendCardProps { */ compareBaseline?: ComparisonBaseline; compareDelta?: number | null; - /** - * v1.4.22 A2 — optional third sub-value rendered alongside `7d` and - * `30d`. Used by the BD-Zielbereich tile so the all-time aggregate - * stays visible after v1.4.22 A1 re-anchored the headline to the - * 30-day window. Pass the i18n key for the label (defaults to - * `charts.avgAllTimeShort`); other tiles leave both fields undefined - * so the third sub-row never renders. - */ - avgAllTime?: number | null; - avgAllTimeLabelKey?: string; - avgAllTimeColorClass?: string; } export function TrendCard({ @@ -117,9 +106,6 @@ export function TrendCard({ trend7Delta = null, compareBaseline = "none", compareDelta = null, - avgAllTime, - avgAllTimeLabelKey = "charts.avgAllTimeShort", - avgAllTimeColorClass, }: TrendCardProps) { const { t } = useTranslations(); const fmt = useFormatters(); @@ -374,35 +360,12 @@ export function TrendCard({ )} - {/* v1.4.22 A2 — optional third sub-value (e.g. all-time aggregate - for BD-Zielbereich after v1.4.22 A1 re-anchored the headline - to last-30 days). Other tiles leave `avgAllTime` undefined so - the third sub-row never renders. - - v1.4.27 MB7 / CF-41 — the third sub-value renders inline - alongside `7d` / `30d` on every viewport. The previous - wave split ` - {t(avgAllTimeLabelKey)}:{" "} - - {avgAllTime !== null ? formatValue(avgAllTime) : "—"} - - - )} + {/* 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 + sub-rows. */}