Commit Graph
2713 Commits
Author SHA1 Message Date
Marc-André Bombeck 4d892f6867 Merge rollup regression accumulators into release/v1.20.0
Store additive per-bucket regression accumulators (sum_x, sum_xy, sum_xx,
sum_yy) so windowed slope, r2 and standard deviation compose from the
rollup tier instead of a live raw-table scan on every warm request
(migration 0190, backfilled by the boot queue). Median and the exact
anomaly count stay on a minimal live query. Parity with the live REGR_*
path within float tolerance, well below the read rounding.
2026-06-22 07:21:56 +02:00
Marc-André Bombeck 4706120879 test(rollups): pin accumulator regression parity with live REGR_*
Two suites pin that the windowed slope / r2 / population-sd composed from
the rollup accumulators equals the live Postgres REGR_SLOPE / REGR_R2 /
STDDEV_POP over the same raw rows.

compose-regression.test.ts (container-free) builds per-DAY-bucket
accumulators from a raw fixture exactly as the populator's SUM terms do,
then asserts composeRegression over those buckets equals a direct OLS over
the flat raw rows - the same closed form Postgres folds - on an uneven
multi-bucket fixture, plus the coverage contract (NULL accumulator,
< 2 readings, and zero-x-variance all return a full miss so the reader
falls back to live SQL).

rollup-regression-parity.integration.test.ts (real Postgres testcontainer)
seeds raw measurements across many uneven DAY buckets, folds the rollups,
and compares the accumulator compose to live REGR_*/STDDEV_POP to <= 1e-9:
a single-source WEIGHT series, a dual-source RHR series proving the
source-collapse runs BEFORE accumulator summation (composed == live WHOOP
-only, != the cross-source blend), and a DST-boundary series confirming
the UTC epoch-day x-axis parity-matches across the transition.

The residual is pure floating-point reassociation (per-bucket-then-window
vs flat summation), orders of magnitude below the read tier's rounding.
2026-06-22 07:20:08 +02:00
Marc-André Bombeck b07fbcb6fa Merge AI client message-array refactor into release/v1.20.0
Move the AI client layer from a single system/user string pair to a
structured system + messages[] shape, with optional tools/toolChoice and
a result carrying toolCalls/finishReason/cachedInputTokens. Per-provider
prompt-caching is wired (Anthropic cache_control, OpenAI/local auto
prefix); tool plumbing is in place for the Coach to use. Behaviour-
preserving: no streaming change, snapshot-once retained.
2026-06-22 07:17:48 +02:00
Marc-André Bombeck 4e777a7e6a refactor(ai): take a structured message array instead of a string pair
Replace the CompletionParams { systemPrompt, userPrompt } string pair with a
structured { system, messages[] } request across all five AI clients. One
refactor unlocks three things: provider prompt-caching (Anthropic cache_control
on the stable system prefix, OpenAI/Codex automatic prefix cache, with
cachedInputTokens surfaced for observability), the tool-calling wire surface,
and clean multi-turn.

Each client maps the new shape to its own wire and degrades unsupported
features silently: the local OpenAI-compatible server never receives a tools
field (most reject unknown keys), and prompt-caching is best-effort everywhere.
CompletionResult gains toolCalls, finishReason and cachedInputTokens; the tool
types and per-provider mapping ship now, the tool-use loop does not.

Vision inputs fold into the user turn's content parts via singleUserTurn, with
each provider's pre-existing block ordering preserved. The corrective-retry
paths append to the last user message rather than a flat string. The Coach
keeps assembling one user turn so its snapshot-once and grounding behaviour is
unchanged. No wire schema visible to API consumers changed.

Behaviour is unchanged across every surface; the full suite stays green.
2026-06-22 07:17:10 +02:00
Marc-André Bombeck 81d318d7dc feat(rollups): re-fold NULL-accumulator buckets on boot backfill
Migration 0190 left existing DAY rollup rows with NULL regression
accumulators until a re-fold refills them. Add a second discovery arm to
enqueueBootTimeRollupBackfill, UNION-ed with the per-day missing-coverage
arm: a user whose DAY buckets carry sum_xy IS NULL inside the fold window
needs a one-time re-fold so the windowed slope/r2/sd readers compose from
the accumulators instead of falling back to the live REGR_* scan.

New writes self-populate the accumulators (they ride the same aggregate
as mean/sum), so the arm drains to empty once the boot re-fold catches up
- the same convergence the v1.4.39 sum_value rollout reached through this
queue. Bounded to ROLLUP_FOLD_WINDOW_MS so days outside the window never
re-flag a user on every boot.
2026-06-22 07:14:06 +02:00
Marc-André Bombeck be37497118 perf(rollups): compose windowed slope/r2/sd from accumulators
The summaries slice (computeFromRollups) and the comprehensive aggregator
(buildFromRollups) each ran a narrow live $queryRaw over the raw
measurements partition on EVERY warm-rollup request to fold slope7/30/90
and r2_7/30/90 via REGR_SLOPE / REGR_R2 - the un-eliminated live scan on
the happy path.

Drop the six REGR_* columns from both narrow queries and compose the
windowed slope / r2 (and the population sd via composeRegression) from
the per-bucket accumulators stored in measurement_rollups. The 90-day
DAY buckets are loaded once (already loaded in comprehensive; one bounded
findMany added in the slim slice), source-collapsed to the canonical
source, then summed over the day-aligned 7/30/90-day sub-windows. A
window whose buckets lack accumulators (pre-backfill) composes to null,
the same shape the empty REGR_* FILTER returned, so a transitional read
never returns a wrong partial regression.

median (PERCENTILE_CONT) and the exact |z|>2 anomaly count stay on the
minimal live query - both are non-composable across buckets. The cold
coverage-miss path keeps the live REGR_* regression. Annotate
slope_source:"rollup" + median_source/anomaly_source:"live" so a
regression to the heavy path stays observable (no-silent-cap rule).
2026-06-22 07:13:06 +02:00
Marc-André Bombeck 7859ca55b3 Merge recovery-gap deepening into release/v1.20.0
Make the physiological return relapse-aware (the start of the final
sustained in-band run, not the first), so a vital that relapsed and
recovered again reports its true recovery. Add an optional sleep-context
sub-line (baseline vs episode asleep-minutes), withheld on thin data and
never feeding the gap median.
2026-06-22 07:07:33 +02:00
Marc-André Bombeck 8d6d63cd1a feat(illness): surface sleep as a context line on the recovery card
Adds a neutral sleep observation alongside the per-episode recovery gap: the
user's usual asleep minutes over their well baseline vs the mean over the
episode window, narrated as 'you slept more/less than usual' when the two
diverge. It is an observation only — never a recovery claim, never a return
track, and never part of the headline recovery-gap number.

Per-night asleep totals come from the canonical night reconstruction over the
baseline and episode windows, so a multi-source night (two writers for one
sleep) is counted once, matching every other sleep surface. The observation
withholds entirely on thin data — too few baseline or episode nights, or a
delta below a 30-minute floor — so day-to-day jitter is never narrated as a
finding.

Additive optional DTO field; no schema, no migration. Two locale keys across
the six bundles. Regenerate the OpenAPI contract from the Zod source.
2026-06-22 07:06:22 +02:00
Marc-André Bombeck f5d259db58 feat(rollups): store per-bucket regression accumulators
Windowed slope, r2 and population sd still ran a live REGR_*/STDDEV_POP
query over the raw measurements partition on every warm-rollup request,
because the per-bucket mean cannot reconstruct an OLS regression over the
raw points in a window. The per-bucket sd/slope/r2 columns are a
within-bucket fold and do not compose linearly across DAY buckets.

Add the four regression-accumulator sums Postgres folds for REGR_* /
STDDEV_POP - sum_x, sum_xy, sum_xx, sum_yy - per (user, type,
granularity, bucketStart, source) over the same canonical-source rows
the live query reads, with the x-axis in epoch days. With n = count and
sum_y = mean*count already stored, all six terms are additive across
buckets, so a 7/30/90-day slope/r2/sd composes from the summed
accumulators with a bit-identical result to the live closed form.

Migration 0190 adds the four nullable columns; nullable keeps it
additive so existing rows read NULL until the boot re-fold refills them
(mirrors the v1.4.39 sum_value rollout). The populator emits the four
SUM terms inside the existing aggregate (no extra round-trip) and
persistRollupRows writes them. composeRegression / composeWindowedRegression
evaluate the closed form from already-collapsed buckets and return a full
miss when any in-window bucket lacks accumulators, so the reader falls
back to live SQL rather than composing a partial window.
2026-06-22 07:01:42 +02:00
Marc-André Bombeck ff06689a1b Merge dashboard discoverability flips into release/v1.20.0
Default the built, data-gated sleep, steps and glucose dashboard tiles to
visible (each self-hides without data) and surface the Vorsorge chart row,
so shipped capabilities are discoverable on the home dashboard. Seed the
demo with an explicit showcase layout.
2026-06-22 07:01:38 +02:00
Marc-André Bombeck b2ff41070c feat(demo): showcase the seeded tiles on the demo dashboard
The demo account seeds rich sleep, steps, glucose, VO2max and workout
data but inserted no dashboard layout, so the home page rendered the
new-account default and under-sold the product on its primary surface.
Persist an explicit dashboard layout derived from the default with the
seeded-rich strip tiles forced on and the daily-verdict hero enabled, so
a fresh seed renders a full, lived-in dashboard for screenshots.
2026-06-22 07:01:03 +02:00
Marc-André Bombeck efd0470bc1 feat(dashboard): surface sleep, steps, glucose and vorsorge by default
The sleep, steps and glucose strip tiles shipped default-hidden even
though their modules are on by default and rich data flows in from the
sync integrations, so accounts had to hunt through Settings to see them.
Flip the three strip tiles to tileVisible:true and the vorsorge chart-row
card to visible:true, matching the VO2max precedent: each tile self-gates
on having data, so empty accounts keep a clean dashboard. Chart rows for
the three metrics stay off — those charts live on the /insights sub-pages.

totalBodyWater, boneMass and oxygenSaturation stay hidden: they carry as
catalogue ids for the iOS layout round-trip only and have no web render
path, so a flip would surface nothing. Note that at the splice point.

Update the silhouette skeleton and layout default tests for the new
counts.
2026-06-22 07:00:56 +02:00
Marc-André Bombeck e432b53e0d fix(illness): report the last sustained recovery return, not the first
A vital or symptom curve that settles, re-deviates mid-episode, and settles
again now reports the FINAL sustained in-band run as the recovery return rather
than the first. The earlier first-return rule locked onto an interim settle and
under-reported the gap whenever a relapse followed. The change is monotone — a
last return is always at or after the first — so no existing recovery number
shifts; it only extends the gap for episodes that genuinely relapsed.

The anchor-after-first-deviation guard is unchanged: an early in-band run-up is
still never read as a return. The symptom-burden track honours relapse
identically over logged days so the headline median never mixes first- and
last-return semantics.

Adds relapse, multiple-relapse, too-short-final-run, ends-out-of-band, and
symptom-relapse engine fixtures plus a cross-episode integration fixture.
2026-06-22 06:59:02 +02:00
Marc-André Bombeck 461190e63e Merge pull request #357 from MBombeck/release/v1.19.2
v1.19.2 — recovery-gap symptom signal, resilience tile, long-range charts, richer device + Telegram data
v1.19.2
2026-06-21 21:32:20 +02:00
Marc-André Bombeck b4bf31a8c9 chore(release): v1.19.2 2026-06-21 21:21:13 +02:00
Marc-André Bombeck c1d05c9e47 chore(openapi): regenerate for v1.19.2 schema additions
Pick up the HR bucket min/max fields, the TELEGRAM measurement source,
and the recovery-gap driver/return additions in one regeneration.
2026-06-21 21:08:28 +02:00
Marc-André Bombeck ad2634e41b Merge data-model additions into release/v1.19.2
Add per-bucket HR min/max (migration 0188) threaded through batch ingest
and the series reader; make telegram_chat_id unique (0189) with a
defensive pre-clean; make migration 0187 rerun-safe. Capture a numeric
measurement from a Telegram reminder reply (source=TELEGRAM, strict chat
binding, self-cleaning); blood pressure stays satisfy-only pending a
guided two-number prompt.
2026-06-21 21:07:46 +02:00
Marc-André Bombeck 5a2d759e50 Merge symptom-severity recovery track into release/v1.19.2
Fold the illness journal's per-day functional-impact curve (falling back
to linked symptom severity) into the recovery-gap as a return track
against the known-constant healthy baseline. It is adverse by
construction, feeds the qualifying-days floor and the gap median, and
withholds on sparse logging. The card names the driver when symptoms
lead ('…before your logged symptoms eased').
2026-06-21 21:07:02 +02:00
Marc-André Bombeck 9c8fb8c7c8 feat(telegram): unique chat binding and numeric measurement capture
Two related changes to the Telegram bot's data path.

One chat ⇄ one account. `User.telegramChatId` is now UNIQUE (migration
0189, defensive pre-clean of any pre-existing duplicate binding). The
inbound webhook resolves the user from the chat id, so a chat shared
across two accounts would route a reply or button tap ambiguously. The
index is over a nullable column, so an unlinked account (null chat id)
never collides — only two accounts binding the same non-null chat id are
rejected.

Numeric measurement capture. A numeric reply to a measurement reminder is
now captured as a Measurement (`source = TELEGRAM`, added to the enum in
0189). On "Erledigt" for a reminder that names a single-value metric, the
bot offers an optional value prompt via force_reply; a numeric reply is
written through the shared `logTelegramMeasurement` helper — same range
guard, canonical unit, cache + rollup refresh as the manual route. The
reply binds to the reminder and user through a TelegramPromptContext keyed
on the prompt message id (the same strict chat/message binding the
mood-note path uses); the userId is never read from the payload. Comma
decimals are tolerated; non-numeric and out-of-range replies get a calm
hint and write nothing; the reply self-cleans on the ~30-min sweep. Blood
pressure (two values) is out of scope for the single-numeric path and
keeps its satisfy-only flow.
2026-06-21 21:06:42 +02:00
Marc-André Bombeck 5c0583133d chore(db): make the 0187 telegram-prompt-context migration rerun-safe
Guard the CREATE TABLE + both CREATE INDEX statements with IF NOT EXISTS so
a re-run on a database that already has the table is a no-op, matching the
guard convention every other recent migration ships with. Semantics are
unchanged — same table, same columns, same indexes.
2026-06-21 21:06:17 +02:00
Marc-André Bombeck 1352ba2aa6 feat(illness): name the symptom curve in the recovery-gap card
When the logged symptom curve drives the recovery gap, the card names it
('you felt better N days before your logged symptoms eased') instead of
the vital-driven phrasing. Mirror the new gapDriverType and the return
track's adverse flag into the client DTO, and add the driver-aware copy
across all six locales.
2026-06-21 21:06:14 +02:00
Marc-André Bombeck 868dfb72bd feat(measurements): add per-bucket heart-rate min/max to the hourly contract
The v1.19.0 hourly heart-rate bucket
(`stats:HKQuantityTypeIdentifierHeartRate:<UTC-hour>`) shipped avg-only:
one PULSE row carrying the hour's mean bpm. Add the hour's min/max so a
client can render the intra-hour range without re-uploading raw samples.

- Two nullable columns on `measurements` (`value_min` / `value_max`),
  migration 0188. Set only on a well-formed hourly HR bucket row; null for
  per-sample readings, per-day cumulative totals, and manual entries.
- Batch ingest threads the spread through the same unit conversion as the
  average and persists / overwrites it alongside `value` on the bucket
  row, pinning it null everywhere else.
- The series reader returns the spread for `kind=pulse` (null on a
  per-sample PULSE row); the OpenAPI Zod contract documents both fields on
  the batch entry and the series point. The avg path is unchanged.
2026-06-21 21:06:09 +02:00
Marc-André Bombeck fc76c39aea feat(illness): fold the logged symptom curve into the recovery gap
The recovery-gap engine reasoned only over passive vitals and ignored
the journal's own per-day symptom series. Add the functional-impact
(symptom-burden) return track: read functionalImpact per episode day
(falling back to the strongest linked symptom severity) and treat it as
one more return against the known-constant baseline (healthy = 0).

The symptom track needs none of the MAD-band / contamination machinery
the passive vitals carry — the band is the constant 0 — so it is more
reliable to fold in than any passive vital. It is adverse by
construction, so it contributes to adverseCoverageDays and the headline
gap median, giving the qualifying-days floor a genuinely
illness-relevant contributor where a weight-only episode had none.

Stability is logged-days-only: a sparse journal that logs impact-0 once
and stops withholds the return rather than fabricating an all-clear from
the absence of further logs. gapDriverType names the dominant return
track, with the symptom curve winning ties as the most illness-specific
signal.
2026-06-21 21:06:06 +02:00
Marc-André Bombeck 38e10b9ca0 Merge multi-year chart range into release/v1.19.2
A multi-year All range was silently truncated to the most recent year by
the DAY-only 365-row cap. Route windows wider than the cap through a new
tiered rollup reader (WEEK/MONTH by span, finer on coverage miss,
uncapped by tier choice); narrow windows stay byte-identical. No silent
truncation — a coverage miss logs and falls through to the daily path.
2026-06-21 21:02:06 +02:00
Marc-André Bombeck 9ea258556d docs(charts): note the multi-year tier step-up in the chart data path
The chart still requests aggregate=daily for windows over 7 days, but
the server now steps the rollup tier up to WEEK / MONTH for ranges past
the DAY cap and returns whole-history coverage. Update the stale
comments that claimed the "All" range was still capped at ~365 daily
buckets; the client fold + bucketTimeSeries downsample the coarse
buckets to the visible range as before.
2026-06-21 21:01:10 +02:00
Marc-André Bombeck 14ba0b013e fix(charts): step the rollup tier up for multi-year ranges
A multi-year "All" range read its series through the DAY-only daily
reader, which caps the result at BUCKET_CAP.daily (365) buckets. The
older history was silently dropped — the range collapsed to roughly the
most recent year instead of the whole span.

Add readTieredRollupSeries, which reads the bucket tier matching the
charts own display downsampler (WEEK for 1-2 years, MONTH beyond) and
returns whole-history coverage at that tier. readDailySeries now routes
any window wider than the DAY cap through it, falling finer on a coverage
miss and logging the miss rather than truncating silently. Short and
normal ranges never enter the branch, so the common path is unchanged.
2026-06-21 21:01:04 +02:00
Marc-André Bombeck 559db472fc Merge Oura resilience tile into release/v1.19.2
Surface the captured Oura resilience band (limited…exceptional) as a calm
tile on the recovery surface, with a quiet above/in-line/below-average cue
and a learning state while the series is sparse. Server-authoritative read
of the existing summaries slice; neutral card chrome.
2026-06-21 20:58:42 +02:00
Marc-André Bombeck ede01e5700 Merge L1/L2 cleanup into release/v1.19.2
Correct the sleep-rhythm peer-card comment to state plainly that each
card reads its own window (the captions disclose the per-card night
count); no computation changed. Remove the dead streaming.startedAt and
streaming.reasoning state left after the thinking-disclosure removal.
2026-06-21 20:57:35 +02:00
Marc-André Bombeck 7f40860891 feat(insights): surface the Oura resilience band on the recovery page
v1.19.0 ingests Oura's daily resilience level end-to-end (RESILIENCE,
ordinal-encoded limited=1 … exceptional=5) but no surface rendered it.
Add a calm resilience tile to the recovery page: it names the latest
band, gives a one-line description, and a quiet "vs the recent average"
cue once a few days exist, with a learning note while the series is
still sparse. Resilience is a categorical band, not a continuous score,
so it reads as a dedicated readout rather than a misleading line chart.

Server-authoritative — the tile reads the stored summaries slice (latest
+ mean, both computed server-side) and never re-derives the value. Calm
posture: one neutral card, no green-when-good / red-when-low tint. The
band label and trend cue carry the read in muted text only.

Self-gating: renders nothing without a reading, and counts toward the
recovery-page data gate so an Oura-only account no longer sees the empty
note. Copy lands in all six locales.
2026-06-21 20:57:30 +02:00
Marc-André Bombeck 1bf8f3d661 refactor(coach): drop the unread streaming startedAt and reasoning state
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.
2026-06-21 20:54:39 +02:00
Marc-André Bombeck aa3d7fcdf7 docs(sleep): correct the sleep-rhythm peer-card window comment
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.
2026-06-21 20:54:33 +02:00
Marc-André Bombeck 02ce177ac4 Merge pull request #349 from MBombeck/release/v1.19.1
v1.19.1 — Coach flow, sleep & mood polish, navigation, recovery-gap
v1.19.1
2026-06-21 14:46:12 +02:00
Marc-André Bombeck 67ead71e24 test(e2e): match the coach conversations handoff query string
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.
2026-06-21 14:35:59 +02:00
Marc-André Bombeck 41dab24985 chore(release): v1.19.1 2026-06-21 14:24:39 +02:00
Marc-André Bombeck e2ce064254 fix(illness,sleep): document the sleep average DTO and gate the recovery driver
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.
2026-06-21 14:23:24 +02:00
Marc-André Bombeck b9f396c72a Merge recovery-gap refinements into release/v1.19.1
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.
2026-06-21 14:12:02 +02:00
Marc-André Bombeck 638bc6c835 feat(illness): name the driving vital in the recovery-gap summary
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.
2026-06-21 14:11:20 +02:00
Marc-André Bombeck 36affd5daf fix(illness): gate the recovery-gap floor on adverse-direction days
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.
2026-06-21 14:07:44 +02:00
Marc-André Bombeck 62a4dfdf7e Merge S1/S2 sleep average + mood saturation into release/v1.19.1
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.
2026-06-21 14:02:09 +02:00
Marc-André Bombeck bbff6ab143 fix(mood): full-saturate the time-of-day chart and mood calendar
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.
2026-06-21 14:01:37 +02:00
Marc-André Bombeck c67bd942a2 feat(sleep): add average-per-night card beside sleep debt and chronotype
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.
2026-06-21 14:01:30 +02:00
Marc-André Bombeck 430f7e81a1 Merge C1/C2/C3/C5 Coach UI rework into release/v1.19.1
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.
2026-06-21 13:59:48 +02:00
Marc-André Bombeck 336c9224f4 Merge S3/S4 settings + sidebar nav into release/v1.19.1
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.
2026-06-21 13:59:24 +02:00
Marc-André Bombeck c6f4772606 fix(coach): default to new chat and never dead-end on conversations
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.
2026-06-21 13:59:05 +02:00
Marc-André Bombeck a86abd3662 fix(coach): restore the typing animation for the live turn
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.
2026-06-21 13:58:57 +02:00
Marc-André Bombeck de14238535 feat(nav): fix the sidebar order of the clinical and insight spine
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.
2026-06-21 13:58:43 +02:00
Marc-André Bombeck 5bbb46ef72 Merge C4 Coach token efficiency into release/v1.19.1
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.
2026-06-21 13:57:46 +02:00
Marc-André Bombeck bc1f30fec4 perf(coach): send the health snapshot once per conversation
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.
2026-06-21 13:56:49 +02:00
Marc-André Bombeck 8d1c90b8b6 fix(settings): label the layout nav entry Dashboard to match where it leads
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.
2026-06-21 13:56:38 +02:00
Marc-André Bombeck bbbafca7f4 Merge release/v1.19.0 into main (v1.19.0)
Release v1.19.0
v1.19.0
2026-06-21 13:13:54 +02:00