feat(weight): judge the weight trend against the stored target

The weight tile's arrow, 7-day delta and comparison caption were wired
as "up is bad", so a person below their own target who was gaining saw
every step toward it coloured as a setback. Movement is now judged
against the target they entered: below the band a gain is progress,
above it a loss is, and inside it holding steady is, with a move either
way shown neutral rather than orange. Without a stored target the tile
reads exactly as before; the height-derived band is not treated as a
target because nobody chose it.

The direction is resolved once on the server and published on the
dashboard snapshot as tiles.weightTrend, so the web tile and the native
client colour the same reading the same way. The personal-record
direction reads the same placement helper. The feature payload behind
the briefing, the overview analysis and the Coach now carries the
target band and which way is progress, so a narrative no longer calls a
gain a regression when gaining is the goal. The insights vitals grid
drops weight from its fixed "up is bad" set.

Refs #1006
This commit is contained in:
Marc-André Bombeck committed 2026-09-24 09:22:50 +02:00
1 parent 0c3195fd41
commit 1273a76a63
14 files changed
+577 -24

No files matched your search

@@ -68,6 +68,31 @@ describe("<TrendCard> directionSentiment", () => {
expect(html).toContain("text-success");
});
// #1006 — weight inside the person's own target band: steady is the win,
// and a move either way is neither celebrated nor scolded.
it("paints a hold metric that stayed flat as green", () => {
const html = render(
<TrendCard
{...baseProps}
slope30={STABLE}
trend7Delta={0}
directionSentiment="hold"
/>,
);
expect(html).toContain("text-success");
expect(html).not.toContain("text-warning");
});
it("never paints a hold metric orange when it moves", () => {
for (const slope of [RISING, FALLING]) {
const html = render(
<TrendCard {...baseProps} slope30={slope} directionSentiment="hold" />,
);
expect(html).not.toContain("text-warning");
expect(html).not.toContain("text-success");
}
});
it("paints up-good metric ↓ as orange", () => {
const html = render(
<TrendCard
@@ -64,12 +64,16 @@ import type { DerivedProvenance } from "@/lib/insights/derived/types";
// moved to `use-dashboard-derived.ts` so the page can own the one shared batch
// and feed it to both the wellness strip and this grid. Imported above.
/** Up-is-bad for the vitals where a rise is unfavourable. */
/**
* Up-is-bad for the vitals where a rise is unfavourable. Weight is not one of
* them: which way it should move depends on the person's own target (#1006,
* `src/lib/targets/weight-trend.ts`), and this grid has no target in hand, so
* it stays neutral rather than assuming a fall is good.
*/
const UP_BAD_VITALS = new Set([
"RESTING_HEART_RATE",
"BODY_TEMPERATURE",
"BLOOD_GLUCOSE",
"WEIGHT",
]);
/** Up-is-good for the vitals where a rise is favourable. */
const UP_GOOD_VITALS = new Set(["HEART_RATE_VARIABILITY", "OXYGEN_SATURATION"]);