Skip to content

fix(oura): publish the ring's history drain to LiveState.backfilling so the sync indicators light under a ring - #2373

Open
pipiche38 wants to merge 1 commit into
ryanbr:mainfrom
pipiche38:fix/oura-drain-publishes-backfilling
Open

pipiche38 wants to merge 1 commit into
ryanbr:mainfrom
pipiche38:fix/oura-drain-publishes-backfilling

Conversation

@pipiche38

Copy link
Copy Markdown

Every sync indicator — the Today header capsule and status light, the sync chip, the Sleep, Live and Health "Syncing…" states, the macOS menu-bar row and the #1164 "Pending sync" caption on today's Rest — reads ONE flag, LiveState.backfilling, with syncChunksThisSession beside it. Only BLEManager (the WHOOP offload) ever raised it. The Oura drain stamped lastSyncedAt at completion and nothing else, so under a ring all of those surfaces stayed at rest through every drain: a wearer whose ring was handing over a night saw the same header as one whose ring was idle, and the "handing over history now" beat #245 added to the header never fired for them.

Change

  • Swift OuraLiveSource: enterBackfilling() at fetch start (chunk tally reset, mirroring BLEManager.startBackfilling), one chunk per 0x11 batch summary, exitBackfilling() in finishDrain on every end (caught up, stalled, deadline, no cursor progress) before the completed-offload stamp, so the re-score that stamp triggers sees the flag at rest and does not defer Today's history-wide reads (Community and team-chat notification parity: likely out of scope? #755). A publishedBackfilling latch means the ring only ever lowers a flag it raised — LiveState is one object every source writes into.
  • The five duplicated live.connected = false; live.streamingLiveHR = false link-down writes fold into one markLinkDown() that also closes a drain cut by the drop, so a flag cannot be left up holding the header in "Syncing" until the next drain.
  • Kotlin twin: WhoopBleClient.publishExternalBackfilling(active, chunks) beside publishExternalBattery, a syncSink on SourceCoordinator wired in NoopApplication, and an onBackfilling callback on OuraLiveSource raised / ticked / lowered at the same four points (fetch start, batch summary, finishDrain, stop() + STATE_DISCONNECTED).

Behaviour to expect

A caught-up ring's periodic drain is one GetEvents round trip at bytes_left 0, so the capsule expands and settles within seconds; a drain with a night behind it holds it up for the ~1–2 min a full pull takes, chunk tally ticking. The chained pass after a deadline stop lowers and re-raises it 5 s apart; ChargeSyncIndicator already handles a restart mid wind-down. Every action gated on the flag (onAbortSync, Health's "Sync now") is additionally WHOOP-gated, so nothing strap-only is exposed under a ring.

Not in this PR, noted for parity: the Android Oura drain still stamps no lastSyncAt at completion (the Swift side has since 2026-08), so Android's sync chip reads ✓ from the WHOOP timestamp only. Separate concern.

Verification

  • Strand (macOS) builds; compileFullDebugKotlin clean; doc_comment_lint green.
  • The drain is BLE-driven end to end, so the flag's timing is only checkable on hardware: Hardware: owed — to be confirmed on a ring against the strap log's fetching history from cursor / history caught up bracket with the Today header watched during it.

Refs #245, #2208.

…so the sync indicators light under a ring

Every sync indicator - the Today header capsule and status light, the sync chip, the Sleep, Live
and Health "Syncing..." states, the macOS menu-bar row and the ryanbr#1164 "Pending sync" caption on
today's Rest - reads ONE flag, `LiveState.backfilling`, with `syncChunksThisSession` beside it.
Only `BLEManager` (the WHOOP offload) ever raised it. The Oura drain stamped `lastSyncedAt` at
completion and nothing else, so under a ring all of those surfaces stayed at rest through every
drain: a wearer whose ring was handing over a night saw the same header as one whose ring was
idle, and the "handing over history now" beat ryanbr#245 added to the header never fired for them.

- Swift `OuraLiveSource`: `enterBackfilling()` at fetch start (chunk tally reset, mirroring
  `BLEManager.startBackfilling`), one chunk per `0x11` batch summary, `exitBackfilling()` in
  `finishDrain` on every end (caught up, stalled, deadline, no cursor progress) BEFORE the
  completed-offload stamp, so the re-score that stamp triggers sees the flag at rest and does not
  defer Today's history-wide reads (ryanbr#755). A `publishedBackfilling` latch means the ring only
  ever lowers a flag it raised - `LiveState` is one object every source writes into.
- The five duplicated `live.connected = false; live.streamingLiveHR = false` link-down writes
  fold into one `markLinkDown()` that also closes a drain cut by the drop, so a flag cannot be
  left up holding the header in "Syncing" until the next drain.
- Kotlin twin: `WhoopBleClient.publishExternalBackfilling(active, chunks)` beside
  `publishExternalBattery`, a `syncSink` on `SourceCoordinator` wired in `NoopApplication`, and an
  `onBackfilling` callback on `OuraLiveSource` raised/ticked/lowered at the same four points
  (fetch start, batch summary, `finishDrain`, `stop()` + `STATE_DISCONNECTED`).

Behaviour to expect: a caught-up ring's periodic drain is one GetEvents round trip at
`bytes_left 0`, so the capsule expands and settles within seconds; a drain with a night behind it
holds it up for the ~1-2 min a full pull takes, chunk tally ticking. The chained pass after a
deadline stop lowers and re-raises it 5 s apart; `ChargeSyncIndicator` already handles a restart
mid wind-down. Every action gated on the flag (`onAbortSync`, Health's "Sync now") is
additionally WHOOP-gated, so nothing strap-only is exposed under a ring.

Verification: `Strand` (macOS) builds; `compileFullDebugKotlin` clean; `doc_comment_lint` green.
The drain is BLE-driven end to end, so the flag's timing is only checkable on hardware: to be
confirmed on a ring against the strap log's "fetching history from cursor" / "history caught up"
bracket with the Today header watched during it. Not yet run on hardware.

Refs ryanbr#245, ryanbr#2208.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QrsZczdGcUyQSJVuYZuwUF
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant