The mental-wellbeing instrument card carried a font-medium title and a
gap-4 body, drifting from the medication card it should mirror; bring its
title to text-lg font-semibold and its body to the shared space-y-3.5
rhythm.
Several insight grids left a lone trailing tile stranded at half width
with an empty column beside it. Gate each multi-column grid on having
more than one item so a single tile spans the full row, and give the
fixed five-card mood-correlation grid a trailing-card span so it no
longer orphans on the two-up breakpoint.
Add a little bottom breathing room to the device-score tile so its
rolling-average line no longer reads flush against the card edge.
The H24 / H12 preference only reached makeFormatters().time. Surfaces that
build their own Intl.DateTimeFormat — the sleep hypnogram axis, the workout
list and detail headers — omitted the hour cycle, so an en-locale (or AUTO)
user saw AM/PM even with H24 selected. The security activity feed and session
list rendered raw toLocaleString(), bypassing both the locale and the
preference entirely.
Export hourCycleOptions from format-locale as the single source for the
H12 / H24 / AUTO Intl options and spread it into the self-built formatters,
preserving each surface's locale and timezone. Route the security cards
through useFormatters().dateTime. Drop the date-time field's local copy of
the helper.
Guards: the helper contract, the en-US hour/minute call-site pattern (no
AM/PM under H24, AM/PM under H12), and formatUpdatedLabel fed an H24
formatter for the briefing freshness label.
The comprehensive briefing warm/cron path wrapped generation in a fixed
130 s outer abort, but the per-user response-timeout setting accepts
10-600 s and the AI settings tell users to raise it for slow local
models. Any account whose response timeout was >= ~130 s had every
comprehensive warm clipped by the warm bound, not the provider, so the
generation never landed: no cached briefing row was written and the
dashboard fell to the hard empty state.
Size the warm outer abort from the resolved effective timeout
(`aiResponseTimeoutSeconds * 1000`, falling back to the comprehensive
surface budget) plus a fixed headroom for feature extraction, prompt
assembly, and the cache write. A raised setting now extends the warm and
nightly budgets instead of being silently capped. An absolute ceiling
(the 600 s write-time maximum plus the headroom) keeps a wedged call from
pinning the worker while staying above the largest value a user can set.
Both the nightly loop and the forced on-visit warm read the per-user
setting and use the derived budget.
Make the empty state more useful: record the upstream HTTP status on a
failure marker and classify it (timeout / auth / rate-limit / provider /
format), so the "couldn't generate" state points at the right lever —
raise the response timeout for a timeout, re-check the provider for an
auth rejection — instead of one generic line. Keep-last-good behaviour is
unchanged.
Align the response-timeout field copy with the actual default budget
(~120 s, not ~180 s) across the placeholder, the OpenAPI description, and
the inline note.
The readiness/recovery score-anatomy detail card rendered broken: the
contributor factors were obscured and the method text overlapped the
title and ring on every breakpoint. The provenance explainer no longer
ships a collapsible info glyph; it renders its full method body inline,
but the anatomy view still wrapped it in an absolute top-right box, which
pulled the whole paragraph out of flow and painted it over the centred
title, ring and contributor rows. Render the method caption in normal
flow at the foot of the card so the factors read again and nothing
overlaps.
Move the weekly/monthly review Coach hand-off to the icon-only affordance
pinned top-right, matching the assessment cards instead of a trailing
text button.
Widen the Coach conversation-history page to the standard content width
so the conversation list no longer sits in an artificially narrow column.
The layered self-description standard already carries the canonical
intended-purpose line, the Coach standing note, the emergency note and
the data-posture note. The medication, insights and labs surfaces each
repeated a further context-specific disclaimer footer that added noise
without adding meaning.
Remove the three rendered footers and retire the now-unused
`insights` / `labs` / `medications` disclaimer variants from the shared
component (type, key map and doc). Drop the orphaned selfDescription
keys from all six locale bundles so call-site and reverse i18n coverage
stay in parity.
The new-chat hero carried a standing self-description line. Remove that
instance; the greeting and composer stay. The MedicalDisclaimer component
and its insights/labs/medications placements are unchanged.
Grip strength, pain (0–10 NRS), waist circumference, and waist-to-height
existed in the measurement enum, signal registry, and FHIR export but had
no web entry, no detail page, single non-sex-aware bands, and a split-brain
MCP exposure. This closes those gaps.
Web capture: the four signals join the manual measurement form, so a
web-only self-hoster can log them (the iOS client and the measurements API
already could).
Detail pages: flip detailPage on for the four signals and mount each on the
shared HealthKit metric template (chart, stat strip, Coach-read strip,
assessment card) via routed sub-pages and tab-strip pills, the same detail
spine every other metric carries.
Sex-aware bands: grip strength (EWGSOP2 men 27 / women 16 kg) and waist
(WHO men 94 / women 80 cm) now classify by the profile sex through the
age/sex norm resolver the assessment already threads; the resolver also
returns a sex-specific, age-independent band when no date of birth is set.
Fix the waist-to-height boundary so the NICE >= 0.5 rule is correct (the
last in-range value is 0.49).
MCP single source of truth: decouple the registry mcp facet from the Coach
snapshot — a signal can be MCP-readable without being on the Coach surface.
Mark the four physical signals mcp:true, derive the MCP rich-reads clinical
set from the registry flag instead of a duplicate allowlist, and relax the
invariant to coachSnapshot implies mcp (not the reverse). PHQ-9 / GAD-7 and
the environmental signals stay mcp:false and unreachable. compare_metric and
detect_changepoints now advertise clinical-signal support to match
get_metric_baseline.
The sleep-debt computed-engine InfoHint only rendered in the still-learning
state — but the resolved full-debt card is the common case and where the
figure most needs the 'this is our own estimate, not a wearable's native
number' context. Show it there too. Separately, swap the period-narrative
loading shell's hand-rolled muted divs for the shared Skeleton primitive
so its shimmer matches every other loader.
The top-right Coach hand-off icon button and the info-hint trigger sat
below the WCAG 2.5.5 44px minimum on touch. Grow both hit boxes to 44px
on touch while keeping the visible footprint unchanged — the Coach button
is a fill-less ghost, and the info-hint offsets its padding with an equal
negative margin so inline layout is untouched.
Introduce one canonical "what this is / is not, not a medical device"
statement plus a layered, calm disclaimer set, and apply it on the
surfaces that earn a context note.
- New shared `MedicalDisclaimer` component renders the copy as plain
React text (no markdown renderer, per the XSS posture). Variants:
intendedPurpose, coach, insights, labs, medications, emergency,
dataPosture.
- New `selfDescription` i18n namespace across all six locales: the
intended-purpose line, the per-surface notes, the emergency note, and
an explicit data-posture statement (self-hosted, data stays on your
instance, encrypted at rest, BYOK / local model, no third-party
analytics).
- Apply once per surface, layered not plastered: a calm standing line on
the Coach new-chat hero, one framing line in the Insights layout shell
(covers the briefing and trends), a labs note (ranges vary, only a
clinician interprets), a neutral medications note, and the data-posture
line where the provider key is configured. Mental-wellbeing keeps its
existing crisis copy; records keep their existing note.
- Extend the Coach system prompt (EN + DE) with a self-description ground
rule so the Coach answers "what are you / where does my data go"
honestly and stays non-diagnostic.
App-authored copy was already observational; the few diagnose/treat
tokens are negations, idioms, or framed as the user's own self-reported
statement, so no softening was required.
Register the seven environmental-exposure signals through the signal registry
as a new "environment" kind (correlation-eligible; off Coach/MCP/FHIR in v1),
and fold their daily series into the correlation engine as lagged behaviour
channels. Weather/daylight now pair against mood, sleep, and vitals through
the existing FDR / effect-size / shrinkage gates, so a thin series degrades to
absent rather than a spurious link. All env channels share one ENVIRONMENT
family so two same-day fields never lag-correlate against each other.
Generalise sleep debt onto the existing per-metric source-priority picker
(a new sleepDebt ladder, native-above-computed, default WHOOP > Oura > Polar >
computed) instead of assuming one provider. The rhythm read resolves the active
source off the ladder and carries it on the DTO; when the active source is our
own COMPUTED engine — every user today, since no provider ships a native
sleep-debt balance — the Sleep page explains what the figure means and that it
differs from a wearable's native number. The computed model itself is unchanged
and correct (a short rolling balance of need minus sleep, surplus-credited);
only stale comments that still referenced the old 14-night window are corrected.
Add a per-user toggle that turns off the proactive ambient example suggestions
(the daily seeded opener on the Coach hero). The flag rides the existing coach
notification-prefs blob (coach.ambientSuggestions, default on) — no schema
change — and a shared hook makes it the single source every ambient suggestion
surface consults, so the seeded opener and its fetch both stand down when off.
The weekly/monthly review card rendered an internal methodology line plus the
raw MeasurementType tokens it drew on ('Based on: WEIGHT, BLOOD_PRESSURE_SYS…'),
which read as broken and added no value. Drop the provenance caption entirely so
no enum token is ever user-facing, and hide the card whenever there is no real
generated narrative (no 'preparing' husk), consistent with the hide-when-empty
pattern. The query keeps polling in the background, so the card appears the
moment a narrative lands.
The short-horizon trajectory tile read as a wall of text with no clear value
on the metric pages. Drop it from the weight page and the shared HealthKit
metric page (the opt-in forecast slot is removed entirely, not left dead), and
delete the now-orphaned tile + fan components. The trajectory engine stays for
the Coach narrative; only the metric-page surfacing is removed.
The composer mic advertised dictation but errored on tap in environments where
the Web Speech API cannot serve. Gate the control on the API being present AND
a secure context, map the locale to a region-qualified recognition language,
and retire the button for the session on a permission refusal. When dictation
is genuinely unavailable the mic hides rather than showing a control that
errors on every tap.
The ambient example opener was stretched to the full composer width, leaving
a large empty tail that read as broken. Size it to its text instead (capped at
the full width) and guarantee a single line: the question truncates with an
ellipsis on a narrow phone rather than wrapping or forcing a horizontal scroll.
The sleep page rendered an assessment under the duration/architecture charts
and a second one under the sleep-quality scores. The quality assessment mostly
never generated — most accounts carry no wearable sleep score — so it sat at the
foot of the page as a duplicate empty card or a redundant connect-a-provider
prompt.
Drop the quality-block assessment so the duration card is the page's single
assessment; the quality block keeps its score tiles. Remove the now-unused
grounded-note fallback and its deterministic grading helper, and the orphaned
quality-assessment message keys across every locale.
The per-metric assessment card carried the Coach hand-off as a bottom-right
text button, present on only some pages. Pin it to the card header's top-right
as a single icon button (no text label) with an accessible name and a tooltip,
and wire every assessment surface to it: the bespoke slug pages
(weight, BMI, pulse, blood pressure, mood), the generic metric scaffold, the
medication-compliance card, and the biomarker detail page already on the shared
card. Each opens the Coach with a localised, auto-sent opener narrowed to that
metric's snapshot source.
The button self-gates on the Coach triple (provider mounted, operator flag,
per-user opt-out) and the card only mounts it on the populated branch, so it
never advertises a control that cannot work. The auto-sent opener now
interpolates the localised measurement name rather than a raw token.
Hold the submit handler in a ref updated from an effect and dispatch
the auto-send through a microtask, so the first-turn send no longer
calls setState synchronously inside the effect.
Thread an autoSend flag through the coach-launch flow so a card's
"ask the coach about this" hand-off dispatches its seeded prompt as
the conversation's first turn instead of only seeding the composer.
The flag rides the launch context, the layout mount, the drawer, and
the shared conversation surface, where a ref-guarded mount effect fires
the send exactly once for a fresh thread.
Localize the previously hardcoded English assessment opener in the
metric status card behind insights.coach.assessmentPrompt with a
{metric} placeholder, and localize the resting-pulse coach prefill.
Both keys land in all six locales.
Surface the generation-failure signal in the daily briefing card. When a
held briefing is shown but the last refresh failed, the footer states it
plainly and offers a retry that runs the existing explicit regenerate —
no warm-on-mount. When there is no briefing to fall back on, the empty
state reads "couldn't generate" with the same retry instead of the
generic "no briefing yet". The hint is suppressed when no AI provider is
configured, where that hint already owns the surface and a retry is
futile. New copy lands in all six locales.
Remove the in-app water-intake logging feature: the WATER_INTAKE
measurement type, the per-user daily-goal field, the goal-ring card and
its endpoint, and the related query keys, OpenAPI route, and locale
strings. The feature is dropped from this release; the baseline-drift,
breathing-screening, and labs-changes insight cards are unaffected, and
the pre-existing TOTAL_BODY_WATER body-composition type stays intact.
Add three read-only awareness cards to the Insights overview, each drawn
off an existing engine and each honest about empty state — the section
un-mounts rather than surfacing a husk.
- Health status: surfaces what is drifting from the user's own normal by
folding the personal-band deviations (coincident-deviation engine) with
the dated, sustained level shifts (changepoint detector) at
GET /api/insights/health-status. Awareness only, never a diagnosis.
- Breathing screening: summarises the last 30 nights of the per-night
sleep-breathing-disturbance index plus device-flagged events at
GET /api/insights/breathing-screening. Mirrors the device's own
classification and states plainly that it is a screening signal, not a
diagnosis — discuss with a clinician.
- Lab changes: pairs the two most-recent numeric lab panels and reports
each shared analyte's signed delta and reference-band standing at
GET /api/insights/labs-changes. Neutral framing only.
Each reshaping helper is pure and unit-tested for its present/absent
states. Register the three sections in the insights-layout universe so
they merge in as default-visible, wire them into the overview and the
edit-mode title map, add the query-factory keys, document the routes in
the OpenAPI contract, and translate every new string across all six
locales.
Add a WATER_INTAKE measurement type and a per-user daily goal so the
Insights overview can track water intake against a goal ring. Logging
rides the existing measurements create path (type WATER_INTAKE, unit ml,
summed cumulatively); GET/PATCH /api/hydration read the day total and own
the goal. The goal-ring math (empty / partial / met / exceeded) lives in
a pure helper, free of Prisma and React, and is unit-tested in isolation.
Wire the new type through the measurement enum, value ranges, cumulative
HealthKit handling, categories, display transform, list metadata, record
direction, and the chartable-token allowlist. The HealthKit DietaryWater
import stays on the nutrition hold, so the type carries no import mapping
by design.
Document GET/PATCH /api/hydration in the OpenAPI contract.
Add isError branches with a retry affordance to the mood, illness, and
vorsorge lists so a failed read no longer renders as "no entries yet"
and invites duplicate re-entry, matching the labs and cycle pattern.
Make the admin backups table reachable on a phone with a card fallback,
name the admin language and timezone selects and the AI raw-data switch
for screen readers, and replace the touch-unreachable title= help on the
glucose clinical panels with a tap-capable info popover.
Route the password-strength label through the AA-tuned semantic tokens,
underline the register login link so it is not distinguished by colour
alone, give the labs load error alert semantics, and explain why the
target editor Save button is disabled with aria-invalid and a live
alert.
Subtract both the top and bottom safe-area insets from the full-page
Coach height so the composer no longer slides behind the bottom-nav on a
notched phone, and pad the Coach drawer composer and the ResponsiveSheet
bottom-sheet footer for the home-indicator band.
Raise the cycle and illness day-log severity controls to a 44px touch
floor, add hit-slop to the custom-symptom remove button, give the shared
day-log chips a min height, and let the mood-tag action cluster wrap so a
360px row no longer clips the tag name.
Hide the copy button on insecure-context (plain-HTTP) self-hosts where the
Clipboard API is undefined: a new SSR-safe useClipboardSupported() detector
mirrors the speech-synthesis hook so the button is absent rather than
error-toasting on tap.
Stop the per-message action row compressing below the 44px touch floor on
narrow phones: the icon buttons keep their tap size (shrink-0 + min-w-11) and
the row wraps instead of shrinking the up-to-six targets to ~30px.
Surface an icon-only action row below each message, sharing the row with
the hover timestamp: muted until the bubble is hovered or focused on
pointer devices, and always visible on touch where there is no hover.
User turns get Copy; assistant turns get Copy, Read-aloud, a thumbs-up /
thumbs-down feedback pair (wired to the existing per-message feedback
endpoint), and Try-again. Copy strips inline chart tokens from assistant
prose so they are never pasted. Try-again resubmits the user turn that
produced the reply as a fresh turn, leaving the composer untouched.
Read-aloud only set the utterance language and let the browser fall back
to its default voice, which on most systems is a legacy compact engine
that reads robotically. Enumerate the installed voices, rank them for the
active locale (prefer enhanced/premium, Siri, Microsoft Natural/Online,
Google, and an exact lang-region match; penalise known compact engines),
and assign the winner with a natural rate and pitch. The voice list loads
asynchronously, so subscribe to voiceschanged and re-rank lazily; when no
voice beats the legacy default, fall back gracefully to prior behaviour.
The history search rendered as a thin, short strip that read as an
afterthought. Give it the app's standard full-width input height and
padding so it matches the standalone conversations page and looks
intentional.
Shorten the composer placeholder across all locales to a short, inviting
one-liner and clip the placeholder pseudo-element so a longer hint can
never wrap to a second line inside the single-row composer on a narrow
phone. The textarea value still wraps and auto-grows for real typing.
Fixed-pixel chart bands ate roughly half the screen when a small phone
was rotated to landscape. Swap the brittle heights for viewport-relative
clamps so the chart envelope holds its previous size on desktop and
portrait but shrinks toward a floor on short landscape viewports. Covers
the overview trends-row slot and the mood distribution, weekday, and
time-of-day charts (with matching skeleton heights). Chart series and
styling are untouched; only the container heights flex.
The Sleep view led with an average-per-night headline card that repeated
the same figure and label already shown in the average-sleep tile within
the sleep-rhythm grid row below it. Remove the leading card and let the
tile carry the readout; the stage-distribution bar and duration trend
chart stay in place.
Code-review and design follow-ups before the v1.22 cut. No behaviour change
beyond the items below.
Coach / insights:
- Coach SSE route: a non-provider error raised inside the open producer (e.g. a
DB failure persisting the reply) now records on the wide event and forwards to
GlitchTip instead of masquerading as a provider outage; it still surfaces an
error frame since the stream is open.
- Lab ↔ outcome correlations: hedge faint-tier pairs ("a faint hint, if
anything … too small to lean on") so the narrated confidence never outruns the
effect, matching the daily engine.
- Reminder sweep: mint the plan-review reminder and clear the plan's reviewDate
in one transaction so a half-applied write can't duplicate the reminder.
- Fold the two raw provider-fallback runners onto one shared chain walker.
Design / accessibility:
- Lift sub-44px tap targets to the 44px mobile floor (suggested-action card,
Coach bubble affordances), brighten the resting timestamp clock for non-text
contrast, and route the lab chart height through the shared constant.
- Replace ad-hoc arbitrary type sizes with the scale token and static size-11
icon buttons with the named icon-lg variant.
- Settings: add the missing About card-header descriptions, normalise the
low-stock numeric widths, and relax the notification-status rows from nested
cards to a divided list.
Housekeeping:
- Drop three unused exported types (and the now-orphaned source const), re-stamp
v1.21.5 comments to v1.22, and register the live-judge eval harness as a knip
entry so the script-only module resolves.
Route the composer seed questions through i18n so a German user no longer
gets an English opener when tapping Ask the Coach from a metric page or an
insight card. Replace the relative timestamp on the briefing and per-metric
cards with a freshness label that shows the time only for today, drops it for
yesterday, and renders a locale date honouring the user's date-format
preference for older entries. Render the briefing paragraph, the per-metric
and score assessments, and the period narrative through the shared paragraph
helper so longer prose shows real paragraphs instead of one run-on block.
Adds the insights.coach.seed.* and insights.updated* keys (English + German).
Feed the user's own usual blood-pressure range — the median plus or minus
MAD band, computed from the readings already in the snapshot — into the Coach
context with a clause to quote those bounds verbatim, so a band is never
invented from window means. Thread the stored first name into the prompt and
surface it sparingly on roughly one turn in three, never as a rote greeting.
Add the context-layer rule that names the relevant context before a
suggestion, a single-chart-token allowance that works on every provider, and
a per-turn opener hint.
Wire the already-shipped suggested-action SSE frame to render: the streaming
hook collects it and the message thread shows the action card, mirroring the
reminder-suggestion path. Extend the eval golden set with band-verbatim,
chart-token, and no-overclaim cases, and pin a red-team case proving the
chart-token syntax cannot smuggle a dose past the outbound screen.
Add a "Your reminders" panel under Settings → Coach that lists the stored
reminders, highlights the due ones, and gives the user view, confirm,
done/dismiss, and delete control over their own memory. Light the Coach
launcher dot when a reminder is due, alongside the existing nudge signal.
Add the generic confirm-card component for the suggested-action frame: a
plain-text preview with a localized per-action heading and a single confirm
tap that posts the validated params to the build endpoint.
Translate the new copy in English and German.
The bottom-right launcher opened a blank new chat on an unread proactive
message instead of the thread the Coach wrote. Return the owning
conversation id from the nudge-status endpoint, have the launcher open
/coach?c=<id> on the unread path, and stamp the same deep link on the push
metadata so the notification lands in the exact conversation rather than
relying on a most-recent heuristic.
Activate the dormant chart-token render path for the Coach: an allowlisted
metric:<TYPE> token in a settled assistant turn mounts the self-fetching
HealthChart for the user's own series, gated on the turn's grounded
provenance so a metric the reply never saw never renders and no data is
fabricated. Add a read-aloud toggle backed by the browser Speech Synthesis
API (no dependency, no egress) and a hover/tap timestamp tooltip on each
message bubble. Render message text through the shared paragraph helper.