v1.19.2 — recovery-gap symptom signal, resilience tile, long-range charts, richer device + Telegram data#357
Merged
Merged
Conversation
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 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.
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.
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.
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.
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.
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.
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.
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.
…ontract 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.
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.
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.
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.
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').
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.
Pick up the HR bucket min/max fields, the TELEGRAM measurement source, and the recovery-gap driver/return additions in one regeneration.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to v1.19.1. Two additive migrations (0188, 0189); no breaking changes.
Added
Changed
Fixed
telegram_chat_idunique (migration 0189, defensive duplicate pre-clean, no account removed).Gate: typecheck 0, lint clean, prettier clean, 11179 tests,
next buildOK, OpenAPI in sync. 4-lens QA: ship-ready, nothing above Low. Demo seed already refreshed live.