The oxygen saturation page drew a fixed clinical band, so a person with
a lower target of their own saw their normal range shaded as a warning.
The page now reads the resolved range from /api/user/thresholds and
draws it; without a target the clinical bands stay as they were.
Refs #1261
The Health Score graded the weight pillar against 22 x height squared,
expanded by 2 kg either side: a target nobody set, nothing disclosed,
and the only producer of the pillar's band. For anyone with a height on
file it pre-empted both the honest trend-only path the module header
promised and the null-redistribution behind it, while the target the
user had actually entered sat unread in User.thresholdsJson.
computeHealthScore now takes the band whole plus the yardstick it came
from. Both callers resolve it through one shared builder next to
buildHealthScoreBpInputs, which hands over a band only when
getEffectiveRange reports a real user override; every other account
scores the bare trend. defaultWeightTargetFromHeight is gone.
Same pass, same defect class elsewhere: the dashboard bands, the
weight-drift verdict rung and the weight/BMI record direction all
ignored an explicit target too, and the snapshot never resolved the
mood module, so a mood-off account got a mood pillar in the hero ring
and none in the Insights card. moodEnabled and the weight yardstick are
required parameters now, so a caller that forgets one fails to compile
instead of quietly scoring against a default.
Reference-band fills, classification tones, and the trend-chart palette
carried hardcoded hex that rendered identically in both themes and
glared on the light surface. Route them through the semantic and chart
CSS variables, which already hold per-theme values, so the light theme
gets the muted set and the dark theme stays byte-identical.
* fix(medical): protect against silent DBP hypotension and align BP citations
Three correctness issues that the v1.3.3 ecosystem audit surfaced and
that we deferred until the v1.4 cycle:
1. **Diastolic blood-pressure orange band reached 60 mmHg.** With the
default age-based targets (DBP 70-79), the lower orange wing was
computed as `diaLow - 10 = 60`, putting a sustained reading of
60 mmHg into "mildly low" territory — visually a yellow tile, not a
red one. 60 mmHg is widely treated as a hypotension / perfusion-risk
threshold (general-adult <90/60 from AHA/NIH; J-curve evidence in
ONTARGET / TRANSCEND for treated hypertensives; ESH 2023's explicit
caution against treating below 70 in the elderly). The orange floor
is now clamped at 65 mmHg, so a reading of 60 lands in red. The
user-override path stays untouched and is already audit-logged.
2. **"ESC/ESH 2023" doesn't exist as a joint guideline.** ESC withdrew
from the 2023 authoring; the 2023 hypertension document is ESH-only.
The codebase had a mix of "ESC/ESH 2018" (analytics) and
"ESC/ESH 2023" (AI prompts). Both are wrong in different directions.
Every site (`bp-targets.ts`, `targets/route.ts`,
`ai/prompts/blood-pressure.ts`, `ai/prompts/base-system.ts`) now
cites "ESH 2023" with the published source URL. Target bands are
unchanged 2018 → 2023 — only the citation moves.
3. **"WHO ≥ 8 000 steps/day" was a recurring hallucination.** WHO's
2020 Physical Activity guidelines publish minutes per week, not a
step quota. The cohort study HealthLog actually relies on is
Saint-Maurice et al., JAMA 2020 (mortality plateau 8 000–12 000
steps). The v1.3.3 fix landed in `effective-range.ts` but four AI
prompt strings (`base-system.ts` DE+EN, `general-status.ts` DE+EN)
carried the old wording forward. Same fix shape: cite Saint-Maurice
for the step number and add an explicit "WHO does not publish a
step quota" note so the model doesn't regress. Sleep target moves
from "ESC" to AASM 2015 in the same surfaces — ESC publishes no
adult sleep duration guideline.
Adds `src/lib/medical-citations.ts` as the new single source of truth
for cited guidelines (id, name, year, url, caveat). v1.4 surfaces will
import these constants instead of duplicating strings, and the new
`medical-citations.test.ts` drift-test asserts that every entry has a
non-empty url + caveat, that ids are unique, and that the WHO-as-step-
source hallucination cannot reappear as a constant.
Tests: 358 → 363 (5 new for the master citations list). Typecheck and
existing analytics tests pass. No clinical numbers changed beyond the
DBP orange-floor clamp.
Co-Authored-By: Marc-André Bombeck <noreply@anthropic.com>
* fix(medical): consolidate body-fat ACE bands and document bedtime-glucose source
Two related medical-truthfulness issues from the v1.3.3 ecosystem audit.
**Body-fat classification: ACE bands corrected.** v1.3.3's classifier
used `essential = 6 (M) / 14 (F)` as the *floor* of the "Essential"
category, which is actually ACE's *Athletes* lower bound. Anything
below was lumped under "Essential" — including readings (e.g. 1% male)
that are clinically concerning, not just "athletically extreme". The
classifier now mirrors the ACE table precisely:
- Below essential (M <2 / F <10): danger.
- Essential (M 2–5 / F 10–13): warning.
- Athletic (M 6–13 / F 14–20): normal.
- Fitness (M 14–17 / F 21–24): normal.
- Acceptable (M 18–24 / F 25–31): warning.
- Obese (M ≥25 / F ≥32): danger.
**Three-way drift in body-fat target ranges consolidated.** v1.3.3 had
three sites with three different green-band numbers:
- `value-bands.ts:118-120` returned M{10,20} / F{18,28}.
- `app/api/insights/targets/route.ts:336-338` hardcoded M{14,24} /
F{21,31} with `source: "ACE"`.
- `classifications.ts:155-205` used a third overlapping set of
thresholds for the band labels.
ACE fitness + acceptable (M 14–24 / F 21–31) is the realistic healthy
band for a non-athlete adult and is now the single source in
`getBodyFatTargetRange`. Targets route and chart value-bands import the
helper instead of duplicating the numbers.
**Bedtime glucose citation softened.** ADA Standards of Care 2024 §6
publishes pre-prandial 80–130 and post-prandial <180; the document does
NOT publish an adult bedtime target. The 90–150 mg/dL band HealthLog
ships is reasonable advice (within the daytime pre-prandial range,
slightly raised floor to avoid nocturnal hypoglycaemia) but it's not
ADA-published. The inline comment in `effective-range.ts` now states
the absence of an adult guideline explicitly and cites ISPAD 2022
(pediatric T1D, 80–140) as the closest comparator. Numbers unchanged.
Tests: 363 → 369 (6 new for the ACE bands). Existing assertions
unchanged.
Co-Authored-By: Marc-André Bombeck <noreply@anthropic.com>
* fix(medical): align step-range target with the effective-range source
`getStepsRange()` in classifications.ts returned `{7 000, 10 000}`
while `effective-range.ts` (the canonical resolver behind
`getEffectiveRange`) returned `{8 000, 15 000}`. The two surfaces
showed different "green" bands to the same user — `/api/insights/
targets` used the lower pair, the dashboard chart-bands used the
upper pair.
Aligns both on the upper pair, anchored on Saint-Maurice et al. JAMA
2020 (mortality plateau 8 000–12 000 steps/day). Adds the citation
header to the steps section so the next audit pass can verify in
place. WHO is explicitly called out as NOT a source for a step quota
(only an activity-time guideline).
The classifier function `classifySteps` is unchanged — its 3 / 5 / 7.5
/ 10 k bands match Tudor-Locke 2004 pedometer indices and are kept
distinct from the two-number "target range" helper.
Tests stay at 369/369.
Co-Authored-By: Marc-André Bombeck <noreply@anthropic.com>
---------
Co-authored-by: Marc-André Bombeck <noreply@anthropic.com>
HealthLog — personal health-tracking PWA with weight, blood pressure,
pulse, mood tracking, medication compliance, Withings integration,
doctor report PDF export, and Dracula-themed UI.