Commit Graph
765 Commits
Author SHA1 Message Date
Marc-André Bombeck 1d3ee1d93c test(integrations): expect six integration pills and drop unused field maps
The mobile settings test asserted three integration pills; Polar, Oura and
Nightscout bring the list to six. Remove the unused Oura and Polar field-map
documentation constants and the unused advanced-indices interface the dead-code
gate flagged.
2026-06-14 21:46:20 +02:00
Marc-André Bombeck 713fa3e926 feat(coach): feed native strain and teach the glucose panel
Surface WHOOP's native day strain to the coach on its own 0-21 scale, distinct
from the computed strain score, and instruct the model to prefer the device
number — the gold-standard signal a WHOOP user connected was previously invisible
to the coach. Document the glucose clinical block in the prompt so the model reads
time-in-range, GMI, estimated A1C and variability correctly and never asserts them
while still learning or off spot readings, and extend the non-English unit
vocabulary to cover glucose. Centralize the glucose panel window and run the
independent derived reads concurrently.
2026-06-14 21:27:52 +02:00
Marc-André Bombeck 5f85d20f02 feat(coach): feed native day strain, document glucose panel + center its window
Add WHOOP-native DAY_STRAIN (0-21) to the Coach snapshot as a dedicated
dayStrain block, kept distinct from the computed STRAIN_SCORE (0-100) and
preferred native-over-computed, mirroring recovery resolution. Document the
glucose.clinical panel (TIR/GMI/eA1C/CV) plus the stillLearning and
isSpotEstimate gates in the system prompt, and the native dayStrain scale in
the derived paragraph. Extend the DE and FR/ES/IT/PL evidence-block unit lists
with mg/dL, mmol/L and min so a non-English coach can cite glucose and duration
values. Export GLUCOSE_PANEL_WINDOW_DAYS as the single source for the 30-day
clinical-panel window and import it across the analytics route, dashboard
snapshot, and Coach snapshot. Compute the derived metrics concurrently.
2026-06-14 21:27:12 +02:00
Marc-André Bombeck a198ce0958 fix(onboarding): show the real integrations and set honest expectations
Bring the source step up to date — present the six shipped integrations instead
of a single vendor and a stale coming-soon badge, and signpost the import path
for existing history. Stop promising that the goals and source choices configure
the dashboard, which they do not yet; frame them honestly as helping the user
decide what to track. Set the calibrating expectation on the done screen so empty
tiles read as learning, not broken, and lead forward with connect and log CTAs.
Count any connected integration toward the setup checklist, not Withings alone.
2026-06-14 21:26:41 +02:00
Marc-André Bombeck 1ddc8a0c47 Surface shipped data sources in onboarding and tell the truth
Rewrite the source step to list the live integrations (Apple Health,
WHOOP, Oura, Polar, Nightscout, Fitbit) alongside Withings and manual
entry, each linking to where it is connected; drop the stale Apple
Health coming-soon badge. Add an import affordance for existing history.

Reword the goals and source copy so it no longer promises automatic
dashboard or reminder configuration that does not happen yet. Add a
calibration line to the done screen setting the expectation that some
insights sharpen over the first weeks, and lead the success screen
forward with connect-a-device and log-first-measurement actions.

Generalize the dashboard checklist data-source item so any connected
integration satisfies it, not Withings alone.
2026-06-14 21:26:09 +02:00
Marc-André Bombeck 812b217470 refactor(integrations): collapse the remaining duplication and drop a dead column
Derive the recovery source rank from the one priority ladder instead of a second
hand-kept map, re-point the WHOOP and Fitbit HTTP classifiers at the shared
status classifier, and lift the near-identical poll-cohort job handlers onto one
factory. Drop the Polar refresh-token column, which was only ever written null
since Polar tokens do not expire. Behaviour is unchanged across the recovery,
classifier, queue and route suites.
2026-06-14 21:24:28 +02:00
Marc-André Bombeck 9675d6189d Note glucoseClinical is insights-only, PDF deferred 2026-06-14 21:23:56 +02:00
Marc-André Bombeck ccf4d6b2ad Extract poll-cohort job handler factory 2026-06-14 21:23:55 +02:00
Marc-André Bombeck cf2c83236b Reuse shared HTTP classifier for WHOOP and Fitbit 2026-06-14 21:23:55 +02:00
Marc-André Bombeck 2da7ea0881 Derive recovery rank from the source-priority ladder 2026-06-14 21:23:55 +02:00
Marc-André Bombeck bd9f03805c feat(insights): surface ring/strap HRV and rank wearable activity sources 2026-06-14 21:22:41 +02:00
Marc-André Bombeck a3704f1633 Lift shared HTTP-status classifier for OAuth integrations
Polar and Oura response classifiers were byte-identical copies of the
status-driven verdict, the typed API error, and the invalid_grant probe.
Extract classifyHttpStatus, an IntegrationApiError base carrying the vendor
label, and a vendor-scoped isOAuthInvalidGrant into a shared module; Polar
and Oura now re-export under their existing names and types so call sites
and instanceof checks are unchanged.

Collapse the triplicated classifyXFailure mapping (Polar, Oura, Nightscout)
onto one toFailureKind helper. Behaviour is identical: same classifications,
same failure kinds, same error types.
2026-06-14 21:08:45 +02:00
Marc-André Bombeck ef36a6a81f Cover Polar and Oura sync queues in the dead-queue guard
Only the Nightscout sync queue was asserted in allQueues; the two OAuth-
wearable polls shipped alongside it carry the same v1.4.37 dead-queue risk.
Assert POLAR_SYNC_QUEUE and OURA_SYNC_QUEUE membership, cron, and handler
wiring so a future refactor can't silently drop one.
2026-06-14 20:59:58 +02:00
Marc-André Bombeck d58f0dc699 Redact Nightscout token from recorded sync error
The entries URL carries the access token as ?token=<secret>; a SafeFetchError
embeds the full target URL in its message, and the sync feeds err.message into
recordSyncFailure, which the status endpoint returns as lastError. Strip token
and api-secret query values to =REDACTED before the message reaches the ledger,
so the secret can never surface in the status response.
2026-06-14 20:56:44 +02:00
Marc-André Bombeck 1735205096 Add bpInTargetCount90/SpanDays90 to snapshot extras OpenAPI
buildExtras emits bpInTargetCount90 + bpInTargetSpanDays90, but the
registered extras schema (additionalProperties: false) omitted them, so a
strict iOS decoder would reject the dashboard snapshot. Declare both as
nullable integers to match the runtime shape and regenerate the spec.
2026-06-14 20:54:49 +02:00
Marc-André Bombeck c268ace58c Derive glucose isSpotEstimate from reading density
Hard-coding isSpotEstimate: true mislabels a dense Nightscout CGM stream
(~288 readings/day) as a spot estimate and undersells the data in the
clinical panel and coach. Compute readings-per-day over the covered span
and treat the series as continuous below the spot caveat once it clears a
~hourly bar (24 readings/day); sparse spot fingersticks keep the caveat.
The panel only renders the spot-reading line when isSpotEstimate, and the
OpenAPI field relaxes from a literal-true to a boolean.
2026-06-14 20:54:32 +02:00
Marc-André Bombeck c5a7e06e35 feat(medications): warn on supply with reorder lead time in mind
Move the low-stock trigger from a bare runway threshold to the larger of that
threshold and the reorder lead time plus one dose interval, so a weekly
medication is flagged before its last dose rather than after — early enough to
reorder. The change only moves the alert earlier; no one's threshold shrinks.
Surface concrete dates (runs out on, reorder by) and a running-low vs last-dose
state through the existing badge and channel, never the card colour. A reorder
lead time is configurable globally with a per-medication override, and the glp1
card flag now runs the same runway decision as the daily check instead of a
stale dose count.
2026-06-14 20:40:21 +02:00
Marc-André Bombeck 08aa655be4 feat(medications): make low-stock alert reorder-lead-time aware
The low-stock alert fired when the projected runway dropped below the
user threshold (default 7 days). For a sparse cadence — a weekly
injection's 7-day floor is barely one dose-interval — the warning landed
with about one dose left, too late to reorder before the supply ran out.

Add an optional per-medication reorder lead time plus a user-level
default (0–60 days, default 10) and widen the alert trigger to
max(lowStockRunwayDays, leadDays + cadenceIntervalDays), so the warning
lands before the last dose for any cadence. Because the trigger is a
max() over the user floor it never shrinks anyone's current threshold; a
daily med with no lead keeps its prior boundary.

The push body and the card notice surface concrete dates — supply runs
out around <date>, reorder by <date> — with a last-dose informational
state and the days/units fallback when supply data is thin. Retire the
stale fixed 4-dose count threshold on the GLP-1 details endpoint and
route its card lowStock flag through the same runway evaluation so the
card and the daily cron agree. Surface the lead default in the low-stock
settings card with progressive disclosure; a single medication can
override it.

Additive schema: nullable Medication.reorderLeadDays (migration 0161, no
backfill) and notificationPrefs.medication.reorderLeadDays. OpenAPI
regenerated.
2026-06-14 20:39:06 +02:00
Marc-André Bombeck 6fba9568ab feat(integrations): add Polar and Oura over OAuth
Connect Polar and Oura through full OAuth, mirroring the existing wearable
house-pattern: a signed, expiring, provider-bound state doubles as an httpOnly
cookie, tokens are encrypted on the user, and an hourly job pulls sleep, heart
rate, respiratory and activity into canonical measurements under each source,
idempotently. Polar Nightly Recharge and Oura readiness feed the recovery
series, which now ranks WHOOP, then Oura, then Polar over the computed proxy.
Settings cards, localized copy and the credential env contract are wired for
both; the providers are optional and configured all-or-none.
2026-06-14 20:13:03 +02:00
Marc-André Bombeck 88139fb981 feat(integrations): add Oura Cloud v2 OAuth sync
Connect an Oura account over the Cloud v2 API: an authorization-code OAuth
flow (shared app from OURA_CLIENT_ID / OURA_CLIENT_SECRET) on the same signed
state machinery as Polar. Unlike Polar, Oura issues refresh tokens; the sync
refreshes reactively on a 401, rotating and re-encrypting both tokens, and a
403 stays a hard reauth rather than a refresh.

An hourly poll-cohort job maps daily readiness to RECOVERY_SCORE, sleep to
per-stage durations plus efficiency / HRV / resting heart rate / respiratory
rate, and daily activity to steps and active energy — all tagged source OURA,
idempotent on (userId, type, source, externalId). Readiness flows through the
canonical recovery ladder above the computed proxy. Settings gains a connect /
disconnect card. Tokens are never logged or returned; every egress goes
through safeFetch.
2026-06-14 20:10:35 +02:00
Marc-André Bombeck 31abd307fd feat(integrations): add Polar AccessLink OAuth sync
Connect a Polar account over AccessLink v3: an authorization-code OAuth
flow (shared app from POLAR_CLIENT_ID / POLAR_CLIENT_SECRET) backed by a
stateless HMAC-signed state token doubling as an httpOnly cookie for CSRF.
The callback verifies the signature, expiry, provider binding, and the
byte-exact cookie match, then encrypts the access token plus member id onto
the user row.

An hourly poll-cohort job maps Nightly Recharge recovery, sleep stages,
and daily activity to canonical measurements tagged source POLAR, idempotent
on (userId, type, source, externalId). The recovery band feeds RECOVERY_SCORE
through the canonical ladder, which now ranks native readiness above the
computed proxy. Settings gains a connect / disconnect card. Tokens are never
logged or returned; every egress goes through safeFetch.
2026-06-14 20:08:50 +02:00
Marc-André Bombeck 52bf985734 feat(invite): open invites through a deep-linkable path
Emit invite links as a /invite/<token> path the iOS app intercepts as a
universal link to start a prefilled registration, with a browser fallback that
redirects to the register flow once the token shape checks out. The path is a
thin, side-effect-free redirect — no lookup, no logging, no enumeration oracle
— and carries the same no-store, no-index, no-referrer hardening as the other
secret-bearing path since the token rides in the URL.
2026-06-14 19:36:01 +02:00
Marc-André Bombeck d3ea8d4470 feat(coach): thread sleep-debt and chronotype into the snapshot
Surface the server-authoritative sleep-rhythm read in the coach snapshot
so a reply can reference cumulative sleep debt and the chronotype band the
Sleep page and dashboard already show. The values come from the same
assembler those surfaces use over a fixed trailing-42-day window, never a
recompute, so the coach can only quote what the page renders.

Hold the learning gate: the chronotype band and social jetlag are emitted
only once the signal is ready; a calibrating chronotype is surfaced as such
and never asserted as a band. Document the new fields and minute units in
the system prompt.
2026-06-14 19:34:14 +02:00
Marc-André Bombeck c1eb618dcc Add invite universal-link landing for app deep-link
Encode the admin invite QR as a clean /invite/<hlv_token> universal-link
that the iOS app intercepts via the AASA wildcard matcher. Add the
matching web route as a thin, shape-validated redirect onto the existing
register flow, and open the path on the public allowlist so an
unauthenticated scanner reaches it. A malformed token segment is
indistinguishable from no invite — no database hit, no enumeration
oracle. The token is never logged.
2026-06-14 19:32:19 +02:00
Marc-André Bombeck e561f8d893 feat(glucose): add the clinical glucose panel
Surface a clinical panel beneath the blood-glucose chart: time-in-range on the
consensus bands, the glucose management indicator and estimated A1C, and the
coefficient of variation with its instability flag by default, and the J-index
and the low and high blood-glucose risk indices with a per-context breakdown
behind an advanced disclosure. Below the calibration threshold it shows a calm
learning state, and it always carries the spot-reading caveat. The analytics
read, the dashboard snapshot, the coach and the doctor report all consume one
computed result over a fixed window, so the panel, the assistant and the
printed report cannot disagree.
2026-06-14 19:21:25 +02:00
Marc-André Bombeck 17e0b2434e Pin coach glucose panel to the fixed 30-day clinical window 2026-06-14 19:19:36 +02:00
Marc-André Bombeck a970ba8ab0 feat(sleep): surface sleep debt and chronotype
Assemble a sleep-rhythm read on the canonical per-night reconstruction — the
same midpoints and asleep totals the sleep score uses — that reports a rolling
sleep debt against the age-based need and an MCTQ chronotype with its mid-sleep,
social jetlag and band. A shared assembler caps the chronotype window so the
dashboard and the dedicated read return identical values, and both withhold a
confident reading behind a learning state until enough nights accrue. The sleep
page gains a debt headline and a chronotype card with the deeper detail behind
an advanced disclosure; the sleep score is left untouched so debt reframes the
sufficiency gap without double-counting it.
2026-06-14 19:12:01 +02:00
Marc-André Bombeck 4b9f31f053 Add glucose clinical panel with advanced-index disclosure 2026-06-14 19:11:57 +02:00
Marc-André Bombeck 1ce1c786b6 Cap chronotype window in the shared assembler
Make computeSleepRhythmFromNights own the chronotype look-back so the dashboard
summary (a year of nights) and GET /api/sleep/rhythm (six weeks) return the
same band, mid-sleep, and social-jetlag values rather than averaging different
samples. Derive the free/work day-type from the local wake-day digits so the
classification holds in far-east timezones, and read the shared sleep-rhythm
DTO type into the client hook instead of a hand-mirrored copy.
2026-06-14 19:10:44 +02:00
Marc-André Bombeck d33fed60d4 refactor(integrations): single Nightscout entry-to-measurement mapper
Route the sync write through mapSgvEntryToMeasurement so the
entry-to-measurement shape has one source of truth instead of an
inline copy plus an unused parallel mapper that could drift.
2026-06-14 19:09:52 +02:00
Marc-André Bombeck f5d0b2f696 feat(integrations): add Nightscout CGM glucose sync
Pull continuous glucose readings from a self-hosted Nightscout instance
and store them as immutable BLOOD_GLUCOSE mg/dL measurements
(source=NIGHTSCOUT), giving the glucose panel real CGM density.

- client over safeFetch: public-host pin by default, relaxed only on the
  per-user private-network opt-in; redirect pinned manual + timeout
- credentials encrypted at rest on User; URL is the connect marker
- sync maps SGV entries first-write-wins on (userId,type,source,externalId)
- connect/disconnect/status routes: apiHandler + requireAuth + rate limit;
  connect validates by a live test fetch with clear errors
- hourly poll-cohort job registered in the all-queues registry
- settings card with URL, token, and the private-network toggle
- i18n across all six locales
2026-06-14 19:06:56 +02:00
Marc-André Bombeck 3be7c3788b Serve glucose clinical panel from one engine to all surfaces 2026-06-14 19:03:34 +02:00
Marc-André Bombeck d1ba01548f Surface sleep-debt and chronotype on the sleep page
Compute cumulative sleep debt and the MCTQ chronotype server-side off the
canonical night reconstruction the Sleep Score reads, and render them on
the sleep page: a sleep-debt headline with a calm partial state under the
night threshold, and a chronotype card whose social-jetlag detail and
corrected mid-sleep sit behind an advanced disclosure. Both hold a learning
state on thin data and never assert a total or a band.

Serve the same values to the dashboard summary and a new GET /api/sleep/rhythm
read so the native client renders identical figures. Free vs work nights
default to weekend in the user's timezone until day-type tracking exists.
2026-06-14 19:03:11 +02:00
Marc-André Bombeck 3dfc3fe515 Add J-index and LBGI/HBGI to glucose clinical metrics 2026-06-14 18:55:13 +02:00
Marc-André Bombeck 524626d2bd feat(sources): add Nightscout, Polar, Oura schema foundation
Extend MeasurementSource with NIGHTSCOUT, POLAR, OURA and add the
per-user encrypted credential columns each source needs: a Nightscout
base URL + token plus a private-host opt-in flag, and Polar/Oura OAuth
tokens mirroring the WHOOP columns. Migration 0160 is additive only —
new enum values, nullable columns, one boolean default false.

Register the three sources across the read enum, the integration-status
key set, and the source-priority ladders.
2026-06-14 18:47:14 +02:00
Marc-André Bombeck 9f8f4167e4 feat(sleep): add sleep-debt and MCTQ chronotype on the unified night record
Compose two pure, server-authoritative modules on the canonical per-night
reconstruction rather than a second pass: a rolling sleep-debt that sums each
night's deficit against the existing sleep need, drains as nights age out, and
never mints negative debt; and an MCTQ chronotype from free-day mid-sleep with
the sleep-debt correction and the social-jetlag gap, classified into a band
through the shared estimator. Both withhold a confident reading behind a
learning gate until enough nights accrue.
2026-06-14 18:35:44 +02:00
Marc-André Bombeck edaa345f4e feat(insights): add sleep-debt and MCTQ chronotype math 2026-06-14 18:32:40 +02:00
Marc-André Bombeck 15c0a63c22 feat(glucose): add clinical-metrics core for TIR, GMI, eA1C and CV 2026-06-14 18:30:55 +02:00
Marc-André Bombeck 5b88401935 fix(doctor-report): resolve recovery by night in the user timezone
Pass the report's timezone into the canonical recovery resolution so the PDF
buckets a WHOOP-native row and its computed proxy onto the same physiological
night — the proxy is stamped the day that ended while WHOOP stamps the wake
morning — instead of a raw calendar day. Aligns the printed recovery summary
with the tile, the coach, and the companion feed.
2026-06-14 17:57:21 +02:00
Marc-André Bombeck 5831143e26 fix(insights): collapse recovery by physiological night, not calendar day
WHOOP stamps a recovery on the wake morning while the computed proxy files the
same night under the day that ended, a systematic off-by-one that left both
rows alive as two competing recovery days once the native series was read.
Resolve recovery by a source-aware wake-day key in the user's timezone — the
computed proxy's day is shifted forward to meet WHOOP's wake morning — so one
night collapses to one canonical row without re-dating the stored stamp and
without a backfill. Thread the same resolution through the wellness reader, the
coach nudge trend, and the coach snapshot, which had each read recovery raw.
2026-06-14 17:51:24 +02:00
Marc-André Bombeck 4eee7c1de5 fix(insights): collapse WHOOP and computed recovery to one canonical night
The native WHOOP recovery and the computed proxy stamp the same night on
opposite clocks: WHOOP files the wake-morning instant (day D), the proxy files
noon-UTC of the day that ended (day D-1). Bucketing raw measuredAt by calendar
day leaves both alive as two competing recovery days.

Bucket each source by the user's local wake-day instead — shifting the proxy's
day-that-ended stamp forward one day to meet WHOOP's wake-morning stamp — so a
single night collapses to one canonical row while distinct nights stay
separate. Resolve the coach-nudge recovery trend and the coach-snapshot dedup
through the same path so no surface blends the two series or drops a genuine
native value on a numeric tie.
2026-06-14 17:49:45 +02:00
Marc-André Bombeck 901faaa096 fix(glucose): carry the unit into the coach and align stat rounding
Convert the glucose figures the coach snapshot builds to the user's unit and
carry the unit into the prompt, so the coach speaks mmol/L to a mmol/L user
instead of a bare mg/dL number — the one glucose surface the unit work had not
reached. Aggregate the series stats over raw values and convert each figure
once, matching the detail page and the FHIR export so a multi-reading mean no
longer differs in the last decimal between surfaces. Drop the fixed day count
from the median label, which the windowed median value cannot honour.
2026-06-14 17:49:10 +02:00
Marc-André Bombeck 69e72c7fcc feat(coach): carry glucose in the user's display unit 2026-06-14 17:45:55 +02:00
Marc-André Bombeck 7f7418a759 fix(api): document iOS-consumed routes, harden consent dedup and mint
Add OpenAPI registry entries for the two endpoints the iOS client now depends
on — the measurement series read it charts from and the provider endpoint it
gates coach visibility on — so the contract source of truth covers their
response shapes and CI catches a future drift. Give the consent active-receipt
dedup a deterministic id tie-break so the unique-index build cannot abort on an
identical-timestamp pair, and map a concurrent first-grant unique violation in
createReceipt to the winning receipt instead of a 500.
2026-06-14 17:42:27 +02:00
Marc-André Bombeck 32d275b4aa Harden consent receipts and document iOS series + ai-provider contract
Document GET /api/measurements/series and GET /api/user/ai-provider in the
OpenAPI registry — both are consumed by the iOS client (charts and Coach
visibility gating) but were absent from the spec.

Break migration 0159's active-receipt dedup on (created_at, id) so an exact
created_at tie still leaves one survivor, keeping the unique index build from
aborting 23505 on affected tenants.

Catch P2002 in createReceipt: two concurrent first grants both reach create,
the loser re-reads the winning active receipt and returns it instead of a 500.
2026-06-14 17:42:00 +02:00
Marc-André Bombeck c80450dfac fix(sleep): synthesise efficiency denominator when no IN_BED row
Nights with AWAKE rows but no IN_BED row (a common Apple Health shape)
lost their efficiency sub-score after the canonical reconstructSleepNights
migration, silently reweighting the composite and shifting historical Sleep
Scores. Restore the synthesised asleep + awake fallback on top of the deduped
per-night totals: keep the real IN_BED figure when present, synthesise when
only asleep + awake exist, stay null when neither.
2026-06-14 17:40:29 +02:00
Marc-André Bombeck 555240cd28 fix(consent): close the receipt mint race, rate-limit, and share the back link
Mint the web AI-consent receipt inside a serializable transaction with an
in-transaction recheck, backed by a partial unique index over one active
receipt per user and kind, so two concurrent mounts can no longer leave two
active grants; a duplicate maps to a no-op mint. Supersede an active row
against the server clock rather than the client-supplied timestamp so a
backdated grant cannot invert the audit chain, and revoke through a single
atomic update. Rate-limit the consent endpoints per user. Extract the
hand-rolled sub-page back link into one component and adopt it across the
four pages that duplicated it.
2026-06-14 17:26:36 +02:00
Marc-André Bombeck db770738bf Harden consent supersede against backdated grants and dup rows
Two follow-ups to the active-receipt uniqueness work:

- Migration 0159 now revokes all-but-the-newest active row per
  (user_id, kind) before building the partial unique index. Without the
  pre-flight dedup the CREATE UNIQUE INDEX would abort on any tenant that
  already carries duplicate active grants — exactly the tenants the fix
  targets — and fail the deploy.
- createReceipt stamps the superseded row's revoked_at with a server
  clock instead of the client-supplied signedAt. A backdated signedAt
  would otherwise mark the prior row revoked before its own signed_at,
  inverting the audit chain. The new row keeps the client signedAt.
2026-06-14 17:25:39 +02:00
Marc-André Bombeck cbde0d573f feat(whoop): per-resource cursors, by-id refresh, and full-sync backfill
Give each WHOOP collection its own incremental cursor in a resource_cursors
map so a slow or rejecting collection no longer drags the sync window forward
for its siblings; legacy connections fall back to the shared last-synced
timestamp. The update webhook now carries the record id and triggers a
targeted single-record refresh instead of re-walking the collection, and a
full sync anchors at a fixed deep start so a backfill covers the span an
incremental tick skips. Cursor advances are a single atomic update guarded by
GREATEST so the independent resource queues cannot race each other backward.

Ingest row shape, source tagging, and upsert keys are unchanged.
2026-06-14 17:25:36 +02:00
Marc-André Bombeck e27bd8b9bf fix(insights): unify sleep reconstruction and make WHOOP recovery canonical
Route the derived Sleep Score through the canonical night reconstruction
instead of its own UTC-day grouping, so a night logged by more than one
source is deduplicated to a single writer rather than summed. The score and
the readiness that folds it now read true per-night totals; multi-source
nights drop to their real figure. Emitted minutes and the night stage map
round to integers.

Resolve recovery to one canonical series — WHOOP-native when present, the
computed proxy as fallback — in the wellness reader, the doctor report, and
the coach snapshot, and stop presenting the proxy and the native score as two
numbers. A WHOOP-connected account's recovery now reads the native value on
the tile, the chart, and the printed report alike.
2026-06-14 17:24:59 +02:00