The thinking disclosure was removed, so the coach streaming bubble no
longer reads startedAt or reasoning. Both fields stayed populated by the
send hook but nothing consumed them. Remove them from
CoachStreamingMessage and every writer, accept the additive reasoning
SSE frame as an explicit no-op so the wire type stays recognised, and
update the thread-test fixture. Streaming is otherwise unchanged.
The comment claimed the average-per-night, sleep-debt, and chronotype
cards share one window and cannot disagree. They each read their own
sub-window — the average over the full scorable span, sleep-debt over
its trailing 14 nights, chronotype over CHRONOTYPE_WINDOW_NIGHTS — so
the figures are each internally honest rather than identical, and every
card's caption discloses its own nightsCounted. State that plainly; no
computation changes.
The drawer's Conversations button now hands off to
/coach?view=conversations so the full page opens the conversation list on
arrival; widen the navigation assertion to accept the query string.
Add averagePerNight to the SleepRhythmResource OpenAPI schema so the new
average-per-night card's field is part of the iOS-consumed contract (the
endpoint already returned it; the Zod source had not been updated, so the
in-sync check could not catch the omission).
Tag each vital return with whether it deviated in the illness-adverse
direction and tally only adverse returns when naming the recovery driver,
so a neutral move (e.g. weight drift) can no longer be surfaced as the
vital someone recovered ahead of.
Tighten the cross-episode typical-gap floor to count only active days
carrying an adverse-direction reading (a neutral weight-only episode can
no longer clear it), via a single shared adverse-direction predicate.
Surface the most-frequent returning vital so the card names the driver
('…before your resting heart rate settles') instead of a bare line.
The cross-episode summary already computes per-vital physiological returns
but the card surfaced only one bare line. Thread the dominant returning
vital through the aggregate and the DTO so the copy reads 'you tend to feel
better before your resting heart rate settles' instead of a context-free
gap figure.
The aggregate tallies which vital produced a real return across the
episodes that qualified for the typical-gap median and reports the most
frequent one (deterministic alphabetical tiebreak); the driver is named
only when a gap actually clears the magnitude gate. Zero new compute — the
returns were already in hand. Adds the localized copy in all six locales.
The cross-episode typical-gap median counts an episode toward its
qualifying-days floor via the per-episode coverage `historyDays`, which
counts a day if ANY banded vital has a reading. A WEIGHT-only episode (no
illness-adverse direction) could therefore clear the floor and feed the
median despite carrying no illness-relevant signal, weakening the honest-
sample claim.
Compute a distinct-adverse-day count in the engine — days with at least
one notable reading in the metric's adverse direction — and feed that as
the qualifying-days floor instead. Reuses the engine's single adverse-
direction predicate (now exported as `isAdverseDeviation`); a neutral-
direction vital never counts a day. The insufficient arm contributes 0,
which is correct since its gap is null and never reaches the median.
S1: add an average-per-night sleep card as a third peer beside debt and
chronotype, computed by a shared helper over the same scorable-night set
so the three cannot disagree; grid widens to three columns. S2: restore
full saturation on the two missed mood surfaces (calendar heatmap and the
time-of-day chart), keeping the quiet tint only for no-data cells.
The v1.19.0 saturation fix missed two surfaces: the mood-by-time-of-day
bars and the mood calendar still rendered populated values at 0.55
opacity, reading as a dimmed glitch. Drop the dimming on populated bars
and cells (and the legend swatches); keep the quiet tint only on the
no-data empty state.
Compute the mean asleep minutes per night from the same canonical
per-night data the sleep-debt and chronotype cards read, exposed as a
new averagePerNight field on the sleep-rhythm DTO. Surface it as a third
peer card with identical chrome, so the three tiles sit side by side.
Calm partial state under a four-night floor; localised in six locales.
C1: /coach defaults to the new-chat hero, auto-resuming only on an unread
coach-initiated message. C2: always-visible Conversations + New chat
toolbar. C3: replace the thinking disclosure with the typing-dots into
word-fade streaming animation. C5: the drawer Conversations handoff opens
the history list on arrival so the full view is never a blank pane.
S3: rename the settings nav entry label Layout to Dashboard across all
six locales (route unchanged). S4: fix the main sidebar spine order to
Medications, Vorsorge, Laborwerte, Krankheit, Insights, Coach,
Achievements, with module gating preserved.
Reverse the resume-most-recent entry default: opening the Coach now lands
on the new-chat hero. The only exception is an unread coach-initiated
message (nudge-status `unread`), which auto-opens the most-recent thread so
the user sees what the Coach wrote. The `?c=<id>` deep-link is unchanged.
Add a dedicated, always-visible Conversations button (plus a New chat
button) to the page surface so reaching past conversations no longer hides
in the composer menu. The drawer's Conversations handoff now routes to
`/coach?view=conversations`, which opens the history drawer on arrival, so
the side-view to full-view path always shows the list instead of a blank
pane.
Replace the elapsed-time thinking disclosure with the classic typing
indicator: while a turn is still thinking with no prose, the assistant
bubble shows the pulsing typing dots, then swaps to the streamed
word-by-word prose once tokens land. Drop the now-unused reasoning/
startedAt bubble props and the orphaned disclosure component.
Below the dashboard/measurements/mood/cycle head, the entries now follow one
fixed sequence: Medications, Vorsorge, Labs, Illness, Insights, Coach,
Achievements. A disabled module drops out; the rest keep this order.
Send the full health snapshot only on the first conversation turn (or
when the verbatim history window starts eliding turns); cheap follow-ups
ride the transcript with a one-line grounding pointer. Drops follow-up
snapshot prefix from ~6,150 to ~50 tokens while preserving grounding and
cross-metric correlation.
The full SNAPSHOT block (every cluster plus labs, illness, cycle and the
reference-grounding lines) was interpolated into the prompt on every turn,
so a one-word follow-up re-paid the entire grounding prefix. Ship it only on
the first turn, or once the history window starts eliding turns; on a cheap
follow-up send a short pointer back to the snapshot the model already received
earlier in the same conversation. The prior turns ride the transcript, so
grounding and cross-metric correlation are preserved while a follow-up turn
sheds about 6,000 tokens. Record snapshotSent and promptChars on the reply
annotation so the cut is observable.
The left-nav entry read "Layout" but the editor it opens is the dashboard
layout home. Rename the label across all six locales; the route stays
/settings/layout.
The canonical rotation registry (src/lib/crypto/encrypted-columns.ts) is the
source of truth its guard test cross-checks; add EcgRecording.waveformEncrypted
there (the script already references it) and drop the redundant parallel guard.
Update the sleep/rhythm route test to the v1.19.0 rolling 5-night balance
(nightsCounted 5, debt 300) instead of the old 14-night cumulative sum.
The insights provider-chain integration test mocks @/lib/insights/features but
omitted BRIEFING_FEATURE_WINDOW_DAYS, which the generate route now reads, so the
route threw on access. Add it to the mock.
The v1.19.0 EcgRecording.waveformEncrypted Bytes column was not added to the
key-rotation script, so an operator rotation would strand ECG waveforms on the
legacy key and dropping it would make them permanently undecryptable — the same
class as the v1.18.1 clinical-spine gap. Wire the column into the rotation and
add a schema-driven coverage test that fails CI if any encrypted Bytes column
is ever left out again.
The Telegram reminder flow confirms a dismissed measurement with
telegram.measurementDone; the parallel measurementDismissed string was added
but never wired, tripping the orphan-key i18n guard. Remove it from all
locales.
The chronotype card carried its own section heading on top of the title it
already renders, so 'Chronotype' appeared twice and the tile did not match
its sibling. Drop the standalone heading and bring the card to the same
shape as the sleep-debt tile: identical chrome and a bold primary value with
one secondary line. The chronotype band is the primary value at the same
scale the debt tile uses for its hours figure, with the natural sleep-
midpoint clock as the companion line.
The cumulative model summed every nightly deficit over a 14-night window
with no credit for catch-up sleep, so a sleeper a modest amount short on
most nights carried hours of standing debt that never recovered while the
pattern held. Replace it with a running balance over a 5-night window:
short nights add their capped shortfall, nights above need pay the balance
down at a sub-linear recovery rate, floored at zero. The headline now
reflects what is still owed tonight and shrinks as the sleeper catches up.
Extend the self-cleaning Telegram bot beyond medication. A mood reminder
now carries a 1-5 inline keyboard plus a "Note..." force-reply and a
"Later" action; a tap logs a MoodEntry (source TELEGRAM, server-side,
user resolved from the linked chat, idempotent on a per-tap externalId)
and confirms with a calm callback toast, no chat echo. An optional
free-text note attaches to that entry via the prompt-context correlation.
A measurement reminder carries "Done" (satisfies the cadence) and
"Later" (pushes nextDueAt forward) — completion and snooze only, no value
capture.
Prompts and replies self-clean in ~30 minutes: the window drops from 1h
and moves to a shared helper, the sender schedules an unanswered prompt's
deletion at send time, and the existing 15-min sweep also prunes expired
prompt-context rows. The webhook keeps the shared-secret + timingSafeEqual
+ rate-limit-before-secret posture; every write callback authenticates via
the stored chat binding. The Telegram chat id is added to the egress
redaction denylist.
Add TelegramPromptContext (migration 0187) mapping a bot force-reply
prompt to the entry a free-text reply attaches to, keyed on
(chatId, promptMsgId) and scoped to the owning user. MoodEntry.source
already accepts the free-text TELEGRAM value, so no enum change is
needed.
The notifications page stacked a breadcrumb strip directly above its h1,
so the page read as two headings (the title followed by a channels line).
Remove the breadcrumb so the page starts with a single heading, matching
every other page. Drop the now-unused breadcrumb i18n keys.
The notification preferences build event labels dynamically as
notifications.event<PascalCase>, so the static call-site coverage test
never enumerated them and three event types rendered raw keys:
MEASUREMENT_REMINDER, MEDICATION_INTAKE_SYNC and COACH_NUDGE. Add label
and description for each across all six locale bundles.
A range tab wider than the dashboard's batched series slice (90 / All on a
~30-day batch) re-read the narrow slice instead of fetching the requested
window, so the chart kept showing ~30 days. Thread the batch's coverage
day-span to the chart and read the preloaded slice only when the requested
window fits within it; a wider tab falls through to a real fetch. Also raise
the All-range request window past the old 365-day cap so longer histories
are no longer silently truncated client-side.
Round out the public demo so every feature is explorable:
- Glucose: a 14-day Nightscout-style CGM stream (a reading every 15 min)
alongside the existing spot panel, so the glucose surfaces render fully.
Tagged RANDOM to satisfy the glucose-context CHECK.
- Labs: a second older panel plus qualitative rows (negativ / nicht
nachweisbar) so the list, detail, sort and delete all have content and a
per-analyte trend.
- Illness journal: a resolved cold with a day-by-day symptom curve and an
active chronic condition carrying a recent flare.
- Cycle: opt the demo account in and seed five observed cycles with biphasic
basal body temperature, period flow, fertile-window mucus and OPK, per-day
symptoms, BODY_TEMPERATURE dual-writes, and a cached forecast.
Cleanup extended to the new tables in FK-safe order; the seed stays
idempotent across re-runs.
Move the non-medical disclaimer to the cycle-tracking on-ramp and drop it
from the repeating in-feature surfaces (calendar tab, predictions panel);
delete the now-unused disclaimer component. Align the left ring column with
the tab content rather than the tab strip. Raise the settings subheading
sizes to the standard card-description scale. Reflow the reminder rows so a
wrapping label no longer collides with its button at narrow widths. Unify
the wheel and phase-education tile padding. Cite the symptothermal
double-check primary source under the second fertility sign.
Oura's daily_resilience collection reports a categorical level — limited,
adequate, solid, strong, exceptional. Encode it ordinally (limited=1 …
exceptional=5) into the numeric measurement value so it fits the existing
model with no new categorical column: add the RESILIENCE measurement type
(migration 0186), fetch and map daily_resilience in the Oura sync, and skip
an unknown or missing level rather than coercing it to zero.
RESILIENCE_LEVELS in the Oura client is the single source of truth for the
encoding. The day-keyed externalId keeps a re-sync idempotent so a re-scored
day overwrites in place. Extend the measurement-type label, unit, range,
category, chart-token, and PR-direction maps; add the metric label across all
six locales.
The 0–14d raw band folded same-day readings on a UTC calendar key, so a
late-evening reading slipped onto the wrong day for non-UTC users and
across DST transitions. Key it on userDayKey(tz) instead and make the
timezone a required option so a caller cannot silently fall back to UTC.
The coarser DAY/WEEK/MONTH/YEAR bands stay UTC-anchored on purpose: the
rollup writer and the live aggregator both bucket on the UTC calendar
day and are parity-locked for the read-swap fallback. Document the split
and thread the resolved user timezone from the coach snapshot caller.
Surface the load-bearing first-run vars in docker-compose.yml and the README
quick-start: secret generation, the SESSION_COOKIE_SECURE=false LAN note, the
single persistent volume, and a /api/version verify step. Add an Unraid
Community Applications template plus a NAS walkthrough covering the external
Postgres setup, and a Portainer app template referencing the compose stack.
Drop vendor names from four metric definition bodies (wrist temperature,
falls, six-minute walk, sleep breathing disturbances) and frame the
six-minute-walk and breathing-index readings as screening proxies rather
than the supervised clinical test. Add a primary citation to the cardio
fitness body: each one-MET higher fitness associates with roughly
10-25% lower mortality (AHA, Circulation 2016). Propagated across all
six locales.