The cadence sentence between the page header and the instrument cards
reads as clutter once the recall windows live in each instrument's own
description. The canonical PageHeader description also moves from the
muted tier to foreground: it is content, not meta.
next 16.2.10, @tanstack/react-query 5.101.2, nodemailer 9.0.3,
react-hook-form 7.81.0, @types/node 26.1.0; docker buildx/metadata/
build-push action pins to their current releases.
The landing slims to intro + instrument cards: the combined history card
leaves the main page entirely. Each card body now opens a per-instrument
detail sheet (the med-/Vorsorge card interaction) carrying the last
score + band, the score-over-time trend chart (restored from the
pre-v1.27.6 chart, lazy Recharts, bands surfacing discreetly in the
tooltip), the dated result history with the crisis re-surface, the Start
action, and the instrument's required attribution line — the Verlauf is
deliberately opt-in behind a click, never pushed onto the page someone
arrives at to take stock.
The wizard drives from the registry: option values in source order (the
WHO-5 leads with 5 = all of the time, the SCI with 4), shared vs
per-item anchor groups, and recall-stem captions above the WHO-5 / SCI
item fragments. WHO-5 items and anchors ship VERBATIM from the official
WHO publication in all six locales; the SCI is validated in English, so
every non-English locale presents the English items plus a localized
'validated in English' note (a self-made translation would invalidate
the instrument — the leak-guard allowlist documents this as intended).
Result view: direction-aware follow-up hints (WHO-5 <= 50 points gently
to the PHQ-9 check-in; SCI <= 16 carries the paper's neutral band
wording; PHQ-9 / GAD-7 keep their >= 10 hint). The intro states each
recall window honestly (two weeks vs past month). The Vorsorge screening
group offers all four instruments. Chrome localized in de/en/es/fr/it/pl.
Wire the two new screening totals through the read surfaces on the
PHQ-9 / GAD-7 precedent: signal-registry entries (higher-better
direction, off the Coach snapshot and MCP by construction, no FHIR
coding attached — neither total has a verified LOINC, and the export
skips uncoded signals rather than shipping an invented code), the
scores category cluster, list labels/icons/colours, the generic
insight-chart tokens, a null PR direction (screeners are never
gamified), and the module-gated mental-health section of the doctor
report. The vitals-table exclusion rationale is documented alongside
the existing screener entries.
Two new self-report instruments join the mental-wellbeing engine on the
existing PHQ-9 / GAD-7 infrastructure:
- WHO-5 Well-Being Index: 5 items on the 6-point 5..0 scale over the last
two weeks; the reported total is raw-sum x 4 (0-100) per the official
WHO scoring (WHO/UCN/MSD/MHE/2024.1, CC BY-NC-SA 3.0 IGO). A total of
50 or below follows the WHO comment (below 50 = indication for further
assessment).
- Sleep Condition Indicator: 8 DSM-5-aligned items, 0-4 each with
item-specific anchors, total 0-32, higher = better sleep; a total of
16 or below maps to the probable-insomnia range of the source
publication (Espie et al., BMJ Open 2014;4:e004183, CC BY-NC,
(c) Sleepio Limited).
The instrument registry generalises rather than forking: per-item answer
ceilings, option order/label schemes, recall-stem captions, a reported-
score multiplier, score direction (higher-is-better inverts the follow-up
threshold), per-instrument attribution lines, and the validated-item
locale set are now instrument properties. Scoring stays server-
authoritative; the create route validates the per-instrument answer
ceiling and projects the total onto a COMPUTED WHO5_SCORE / SCI_SCORE
measurement row (never client-attributable), which also auto-satisfies a
planned screening reminder. Migration 0226 adds the enum values
(additive, idempotent). OpenAPI regenerated.
Neither instrument carries a self-harm item; the PHQ-9 item-9 crisis
routing is unchanged.
The Coach has extracted goal / if-then plan proposals since v1.21.3 —
written as status "proposed", never activated without the user — but no
surface ever showed them, so nothing could be confirmed and the active-plan
memory block stayed permanently empty. This lands the missing user half:
- Chat thread: proposal confirm cards at the tail of the open conversation
(proposals land asynchronously via the post-turn memory-refresh worker,
so the block slow-polls the proposed list while the thread is mounted and
only shows proposals born in that conversation). Accept activates the
plan — it enters the Coach's snapshot memory from the next turn on;
decline soft-deletes the proposal so it never activates and stops
counting against the per-user cap. A settled card collapses to a
one-line confirmation.
- /coach/plans: the management ledger, a sibling of /coach/conversations
reachable from the composer's + menu. Groups proposals (confirm /
decline), standing plans (goal met / end), and the settled history
(remove). No prose editor by design — the extractor is the only writer
of plan text and the PATCH contract only moves the lifecycle.
- Shared use-coach-plans hook + centralised coach-plans query keys, so the
thread cards and the page invalidate each other's reads.
- Both coach-gate invariants (operator master flag, per-user opt-out)
register the new page; i18n keys land in all six locales.
SSR tests pin the card contract: only same-conversation proposals render,
undecryptable rows are skipped, both actions are real buttons.
Groundwork for the plan confirm/manage UI. The CoachPlan pipeline has been
complete on the server since v1.21.3/v1.22 — extractor writes proposals
(status "proposed", per-user cap, de-dup, encrypted prose), the PATCH
confirms proposed -> active, buildCoachPlansBlock injects the top-6 active
plans into the snapshot, and the review sweep drives the reviewDate
lifecycle — but no surface ever listed or confirmed a proposal, so the
extraction spend had no user-visible outcome.
Two additive contract changes the UI needs:
- GET /api/coach/plans accepts ?scope=open|past|all (open = proposed +
active + review_due, past = met + abandoned + reviewed, all = no status
clause) so the management page reads its ledger in one round-trip.
Mutually exclusive with ?status=; both omitted keeps today's
non-terminal default unchanged.
- Plan items now carry sourceConversationId so the chat thread can show
only the proposals born in the open conversation.
OpenAPI regenerated; route tests cover the scope mappings, the
status+scope 422, and the provenance passthrough.
The doctor-report prefs route resolves every toggle to the full
canonical shape; the fixtures predate the allergies/familyHistory
sections and asserted the seven-key shape.
The PHQ-9's validated functional-impairment item left the web wizard
with the review step; the API kept the field but no web client captured
it. It returns as a regular question 10 of 10 in the same style as the
nine scored items: optional per the instrument (submit stays armed
without an answer, a second tap clears the selection, a short hint says
skipping is fine) and never scored into the total. The answer rides the
existing functionalDifficulty field; GAD-7 keeps its seven steps. The
validated wording returns to all six locales.
The over-scroll class keeps recurring (last instance fixed in the
settings shell): a page-level viewport-height reserve or bottom gutter
inside the scrolling main stacks on the shell-owned padding and the
page scrolls past the last card into an empty band. Pin the invariant
instead of the instance: across the settings routes (short hub, long
sortable-list subpages, long form section) at desktop and phone
viewports, main.scrollHeight must stay within the last content
element's bottom edge plus the shell-owned padding and a small
tolerance. Descendants of nested max-h scroll containers are excluded
from the content measure — their overflow never contributes page
scroll height.
The override block's prose mentioned an opacity-suffixed dracula
utility in a form that embedded the CSS comment terminator, cutting the
block comment short and failing the stylesheet parse. Reword the line;
values are unchanged.
The briefing spotlight carried a separate underlined 'open Insights'
text link that thickened the card; every signal row below already
deep-links. Drop it and make the Tagesbriefing heading itself the link
— quiet, no underline, hover lift only — targeting the briefing card
directly via a new #daily-briefing anchor on the insights section
(scroll-mt-28, the app-wide offset token).
After the broad Insights button left the hero, the scoreDrop and
briefing verdict sentences were dead ends. Link-CTA verdicts without a
button label now make the sentence itself clickable (same quiet
treatment): scoreDrop routes to /insights, the briefing sentence
deep-links to the picked finding's metric sub-page via the METRIC_HREF
map (fallback /insights). Verdicts with their own action keep their
button.
The ring toggles rode the section's draft/Save machine; combined with
the server snapshot cache and client staleness this read as 'flipping
does nothing, rings appear later'. Ring choices now bypass the draft: a
dedicated mutation PUTs the server layout copy with only
selectedScoreRings changed (an open draft's other edits stay unsent,
its ring slice is kept in sync), optimistically updates the widgets
query, rolls back on error, and invalidates the dashboard snapshot on
success. The other layout controls keep the draft/Save flow.
Selected rings are now reorderable with the same drag-handle + arrow
interaction as the widget rows below — the array order is the hero
render order, which the server already preserves. Unselected rings
stay plain switch rows and disable at the three-ring cap.
The MED_COMPLIANCE hero ring carried the pooled 7-day adherence
percentage — a mystery number next to today-focused scores. It now
answers the question the hero poses: how many of today's doses are
done. The resolver reads takenToday/scheduledToday off the snapshot's
own medsToday block (one shared builder call, no second tally); score
stays the rounded 0..100 progress for wire compatibility and a new
additive doses field carries the taken/scheduled pair. No dose
scheduled today means no ring, and the band is progress semantics —
green once everything is taken, yellow while doses remain, never red
(a pending morning dose is not an alert state).
The web ring renders the tally (1/3) with a doses label over the ring
system's constant green arc — the same green family the compliance
surfaces in Insights paint — instead of a band gradient; the derived
rings keep their exact insights hues. ScoreRing gains documented
valueText/ariaLabel overrides for tally-shaped centres. OpenAPI
regenerated; labels land in all six locales.
The structured Allergy / FamilyHistoryEntry stores reached FHIR export
only: the doctor-report PDF shipped without the two sections every
clinical intake asks for first, and the Coach knew allergies only from
the free-text answer — a penicillin allergy recorded in the structured
records UI never reached either surface.
Doctor report: two new section toggles (allergies, familyHistory; both
default ON, same stance as labs) across the prefs schema, the export
selection, the aggregator, the PDF renderer, and the export panel. The
tables render stored fields only — substance/category/kind/severity/
reaction/status and relationship/condition/age-at-onset; the reaction
decrypts fail-soft per row and free-text notes are never read, matching
the illness-journal stance. OpenAPI regenerated for the extended
sections shape.
Coach: the about-me context block now appends the structured rows as
clearly labelled entries (authoritative over the free-text answer on
conflict); the projection is a pure composer with unit tests. New
section labels land in all six locales.
Nine insights/cycle cards hand-rolled their icon+title rows at drifting
sizes and colours: the sleep trio (debt, chronotype, average) and the
glucose clinical panel led with h-4 accent icons, the cycle module
(predictions, BBT chart, symptom patterns) coloured its header icons
text-primary against both header contracts, and the hypnogram,
correlation card, and mood correlation cards carried bespoke title rows.
All of them now compose TileHeader.
TileHeader grows the two documented gaps instead of a fourth header
language: an optional icon (text-only tile titles) and the ONE sanctioned
compact variant size="sm" (icon h-4 w-4, title text-sm) used by the dense
correlation tiles. The sleep-rhythm guard test now pins the tile-header
slot instead of the retired accent icon colour.
Eight data surfaces still rendered a read failure as a bare
text-destructive paragraph with no retry: the biomarker manager, the lab
values list, the three record managers (allergies, family history,
conditions), the illness episode detail, and the intake history table;
the lab biomarker detail carried an inline near-duplicate of the
primitive from before it existed. Replace all of them with
QueryErrorCard wired to the query's refetch, keeping each surface's own
error string. Inline role="alert" text remains the pattern for mutation
errors inside forms.
The Card has been a gap-based flex column since v1.4.33 (gap-4 md:gap-6;
CardHeader carries no bottom padding), but 29 CardHeader pb-0/1/2/2.5 and
8 CardContent pt-6 overrides from the padding-era shadcn card survived.
Under the gap model they invert their intent: pb-* renders gap PLUS
padding (looser, not tighter), pb-0 is a no-op, and pt-6 on a header-less
CardContent stacks on the Card's own py-4/6 and makes the card top-heavy
(40-48 px top vs 16-24 px bottom). Delete them all; a genuinely tighter
header-to-body distance is expressed on the Card itself (className="gap-2"),
the only knob that controls it. An ESLint guard for these slots is a
follow-up.
The rule only matched stock-hue-plus-step utilities, letting three colour
escapes through CI: raw dracula-* utilities, hex/rgb/hsl/oklch literals in
colour-shaped JS props (the Recharts color/stroke/fill blind spot behind
the four hex chart colours just fixed), and Tailwind arbitrary-value
colours (bg-[#...]).
Add all three as selectable checks. color-props and arbitrary-value run at
error; the dracula check ships as a warning under a second registration
(healthlog/no-dracula-utility) because ~250 legacy sites predate the token
system — the light-theme overrides keep them readable, and the check moves
to error once the semantic sweep retires them (staged plan in the rule
header). Exempt: global-error.tsx (renders without the stylesheet), the
themeColor viewport metadata (matched by ancestry, not by file), test
files. Add a RuleTester suite covering all four detectors and the
exemptions.
grip-strength, waist, waist-to-height, and pain shipped stock hex chart
colours (#22c55e / #0ea5e9 / #8b5cf6 / #f97316) while every sibling page
passes var(--token) — the four charts ignored both themes and any token
retune. Map them to var(--success) / var(--info) / var(--chart-1) /
var(--warning) and narrow the scaffold's color prop to a `var(--…)`
template-literal type so a raw hex no longer typechecks.
Only --dracula-yellow carried a light (Alucard) override; the other seven
primitives kept their dark neons, so every legacy text-dracula-* utility
rendered at 1.2-1.6:1 on the white card. Each override is tuned to the
semantic light palette already in the block (same hue family) and measured
at >=4.5:1 against both --card and --background; the measured ratios are
documented per line. The semantic sweep that retires the raw utilities
stays a follow-up.
The scoreDrop/briefing specs asserted the removed overview-only button;
they now pin its absence, and the orphaned action key leaves all six
locale bundles (the reverse-coverage guard flagged it).
The scoreDrop/briefing verdict button only opened the Insights
overview, which the health-score card already links — gone. The
briefing-spotlight rows now deep-link to their metric's sub-page via
the shared METRIC_HREF mapping; metrics without a routed page fall
back to the overview.
The Vorsorge card resolves both rules at once: the action button keeps
its constant look in every state (v1.27.5), and a screening reminder's
action reads 'start the check-in' instead of the numeric measure
wording (this wave).
The hero's dose text row gives way to a ring row: the selected score
rings render at the health-score ring's sm flat geometry, readiness /
recovery / sleep in their wellness-strip hues and the 7-day adherence
ring in band semantics — the ring that carries the dose row's
information role. Rings self-gate on data, the verdict line above is
untouched, and the skeleton mirrors the new geometry (plain card shell,
no dose pill, the always-present health circle reserved). Settings ->
Dashboard gains a picker under the overview toggle: up to three rings,
module-gated rows, switches disable at the cap, persisted through the
existing widgets PUT. Locale bundles drop the retired dose-row strings
and pick up the picker copy in all six languages.
Resolve the selected rings server-side and publish them as scoreRings
on the dashboard snapshot (additive, optional for cached payloads).
READINESS, RECOVERY_SCORE and SLEEP_SCORE go through the same
computeDerivedMetric dispatcher the derived batch route calls, with the
profile loaded once via the shared loader; MED_COMPLIANCE pools the
7-day adherence across active non-PRN medications through the canonical
compliance engine (taken over taken-plus-missed, skips out of the
denominator) and bands on the targets-tile thresholds. Rings gate on
their owning module plus the insights gate for the derived trio, drop
out when no data backs them, and resolve fail-soft per ring so a
throwing engine never sinks the snapshot. OpenAPI regenerated.
Add selectedScoreRings to the dashboard layout blob: a closed four-id
set (READINESS, RECOVERY_SCORE, SLEEP_SCORE, MED_COMPLIANCE) capped at
three rings, with a client-safe ring-to-module map alongside. The
resolver drops unknown ids, dedupes, and clamps; a legacy blob without
the field defaults to the single MED_COMPLIANCE ring while an explicit
empty array is respected. The widgets PUT validates the field against
the closed enum and preserves the stored value when a client omits it,
the same contract heroVisible already rides.
PUT /api/measurements/[id] was the one write surface that skipped the
per-type plausibility bands — POST, batch, CSV import, Apple export,
Telegram and MCP all enforce VALUE_RANGES — so an implausible edited
value flowed unchecked into rollups, the health score, the BP gates
and the Coach snapshot. The route now validates the new value against
the row's own type after the lookup and returns the standard
multi-issue 422 envelope on violation.
Value edits on server-owned sources (anything outside
WRITABLE_MEASUREMENT_SOURCES: connector, import, computed rows) are
refused with 409 and the stable errorCode
measurement.update.server_owned_source — editing a provider-attributed
number forges a reading the server never received. Timestamp and note
edits stay allowed, and the edit sheet shows a localised message for
the new code.
The schema's generic min(0).max(500000) bound is dropped: the per-type
band is stricter where it matters, and min(0) blocked legitimate edits
of the signed types (ANS_CHARGE, BODY_TEMPERATURE_DEVIATION). Route
tests cover out-of-range 422, negative signed-type accept, the
server-owned 409, a timestamp-only edit on a server-owned row, and the
happy path. The PUT endpoint is not part of the published OpenAPI
surface — the generated contract is unchanged.
PHQ9_SCORE / GAD7_SCORE join the Vorsorge reminder type allow-list as
their own "Seelisches Wohlbefinden" group, with a 4-week default
cadence (both instruments cover the last two weeks; 28 joins the
interval presets). The reminders ride the existing activity-aware
engine unchanged: they fire only when overdue, and a completed check-in
auto-satisfies them — the assessment route now kicks the eventful
reminder-satisfy worker, which resolves against the server-written
score row; the 15-min cron stays the idempotent safety-net. The
screening reminder's primary action opens the check-in page (a score is
never typed in), and the dispatcher gates the reminder on the opt-in
mental-health module so a disabled module never nags. OpenAPI spec
regenerated for the widened MeasurementReminderType enum.
The recall/forward-look block read as a second briefing paragraph
above the structured list and pulled the card down — the card now
starts straight at 'signals of the day'. The section titles read in
the regular foreground, and the card's Ask-the-Coach corner entry is
gone; the Coach keeps its own surfaces.
The muted-foreground token is AA-tuned in both themes; the /70 and /80
opacity modifiers silently undid that (2.9-4.2:1 measured). Every
informational text node drops the modifier for the full token: sleep
hypnogram captions, learning-gate caveat, integration-card setup hints,
module lock notes, glucose panel and token-footer meta, invite-token
hint, wizard hint, notification channel labels and disabled notes (the
disabled state reads through the note and the disabled switches, not
through dimming), plus the resting state of the timestamp, suggestion
chip and sidebar-collapse controls. User-authored mood notes move to
foreground text-sm — user data is never muted. Decorative non-text
strokes (sparkline grid, calendar dot, titration connector) keep their
opacity; icons keep the /60 graphics floor.
Three strings still promised features "with v1.5" that shipped long
ago: the sleep-stage placeholder and the sleep empty state now say
plainly that the view fills in once a connected source (or a manual
entry) delivers data, in all six locales. The unused Coach
voice-coming-soon string is removed — voice input exists. UI copy
carries no version promises; a release lands when it lands.
The shared formatting contract asks every narrative surface for short
blank-line paragraphs, but two field-level instructions overrode it:
the assessment OUTPUT FORMAT pinned 'flowing prose (2-4 sentences)'
and the briefing paragraph asked for 'a connected 70-160 word read' —
so both consistently arrived as one unbroken block regardless of the
contract. Both now spell out 1-3 short paragraphs separated by a
blank line written as \n\n inside the JSON string.
The recovery detail page rendered only the ring — the one wellness
score without a composition view, although the COMPUTED score IS the
readiness blend persisted nightly. The wellness-score read now attaches
the blend's per-factor sub-scores (RHR, HRV, sleep, respiratory rate,
mood stability — same engine, same weights, resolved server-side on the
same request) when the canonical latest row is the COMPUTED proxy, and
the anatomy view renders them through the identical ranked contributor
rows the readiness and sleep-score composites use, reusing their
labels. A WHOOP-native percentage is not our blend, so it keeps the
row-less presentation; a gated blend simply omits the breakdown. The
new value field is optional and additive — existing clients decode
unchanged.