Skip to content

Add Today pull-to-sync - #426

Merged
ryanbr merged 1 commit into
ryanbr:mainfrom
kavemang:agent/today-pull-to-sync
Jul 16, 2026
Merged

ryanbr merged 1 commit into
ryanbr:mainfrom
kavemang:agent/today-pull-to-sync

Conversation

@kavemang

Copy link
Copy Markdown

Summary

  • Add pull-to-refresh on Android Today to request a manual strap history sync.
  • Gate the gesture with the same connected + bonded + not-backfilling rule used by the BLE client.
  • Keep the refresh spinner short-lived and let the existing syncing note/chip show longer offload progress.
  • Add a focused Today explainability test for the pull-to-sync gate.

Why

Fixes #334.

Today already surfaces sync progress, but the user had to leave the home screen and find another explicit sync action to request fresh strap history. The backend entry point already exists (AppViewModel.syncNow() -> WhoopBleClient.syncNow()); this wires the Today gesture to that safe manual-sync path.

The UI does not try to bypass BLE readiness. If the strap is disconnected, not fully bonded yet, or already backfilling, pulling does not start a new sync or leave a spinner hanging.

Test

  • cd android && ./gradlew :app:testFullDebugUnitTest --tests 'com.noop.ui.TodayExplainabilityTest'

@kavemang
kavemang marked this pull request as ready for review July 14, 2026 18:43
@kavemang
kavemang force-pushed the agent/today-pull-to-sync branch from 72840f9 to 45efa15 Compare July 15, 2026 05:54
@ryanbr
ryanbr merged commit a731d55 into ryanbr:main Jul 16, 2026
1 check passed
ryanbr added a commit that referenced this pull request Jul 16, 2026
…not just a UI reload (#334)

Android #426 added Today pull-to-sync. iOS's LiquidTodayView already had a pull
gesture (handlePull), but it only reloaded UI data (repo.refresh + load) — it
never requested a fresh strap history sync. Wire ble.syncNow() into that existing
refresh action so a pull kicks a manual offload too, matching Android.

syncNow() is internally gated (connected + bonded + not-already-backfilling), so
a pull while disconnected or mid-offload safely no-ops — the same guard Android
routes through WhoopBleClient.canRequestSync.

Observe BLEManager (injected at the app roots beside model), NOT AppModel: AppModel
@publishes bpm on the ~1 Hz HR tick, so observing it would re-render all of Today
every second (the exact churn the LiveState leaves isolate). BLEManager only
publishes connect/discovery state, never HR.

App-target Swift (shared iOS + macOS); validated via app-build.
ryanbr added a commit that referenced this pull request Jul 16, 2026
material3 1.2.1's rememberPullToRefreshState captures the enabled lambda ONCE
(rememberSaveable, no rememberUpdatedState), so { canPullToSync } froze the plain
Boolean from the FIRST composition. Today usually first composes before the strap
has (re)connected, so the gesture stayed disabled for the whole session and
pull-to-sync (#426) silently never worked in the common cold-launch flow.

Read the stable liveSnap State live inside the enabled lambda instead, so each
gesture check sees the current connected/bonded/backfilling. Safe either way
(syncNow is triple-gated); this just lets the gesture enable once the strap is ready.

compileFullDebugKotlin + TodayExplainabilityTest green.
ryanbr added a commit that referenced this pull request Jul 16, 2026
…d-gate (#502)

* iOS/macOS Today: pull-to-sync — a pull now requests a strap offload, not just a UI reload (#334)

Android #426 added Today pull-to-sync. iOS's LiquidTodayView already had a pull
gesture (handlePull), but it only reloaded UI data (repo.refresh + load) — it
never requested a fresh strap history sync. Wire ble.syncNow() into that existing
refresh action so a pull kicks a manual offload too, matching Android.

syncNow() is internally gated (connected + bonded + not-already-backfilling), so
a pull while disconnected or mid-offload safely no-ops — the same guard Android
routes through WhoopBleClient.canRequestSync.

Observe BLEManager (injected at the app roots beside model), NOT AppModel: AppModel
@publishes bpm on the ~1 Hz HR tick, so observing it would re-render all of Today
every second (the exact churn the LiveState leaves isolate). BLEManager only
publishes connect/discovery state, never HR.

App-target Swift (shared iOS + macOS); validated via app-build.

* Android Today pull-to-sync: fix stale enabled-gate (re-review of #426)

material3 1.2.1's rememberPullToRefreshState captures the enabled lambda ONCE
(rememberSaveable, no rememberUpdatedState), so { canPullToSync } froze the plain
Boolean from the FIRST composition. Today usually first composes before the strap
has (re)connected, so the gesture stayed disabled for the whole session and
pull-to-sync (#426) silently never worked in the common cold-launch flow.

Read the stable liveSnap State live inside the enabled lambda instead, so each
gesture check sees the current connected/bonded/backfilling. Safe either way
(syncNow is triple-gated); this just lets the gesture enable once the strap is ready.

compileFullDebugKotlin + TodayExplainabilityTest green.
@kavemang
kavemang deleted the agent/today-pull-to-sync branch July 16, 2026 07:16
DX23876 pushed a commit to DX23876/noop that referenced this pull request Jul 22, 2026
Co-authored-by: kavemang <kavemang@users.noreply.github.com>
DX23876 pushed a commit to DX23876/noop that referenced this pull request Jul 22, 2026
…enabled-gate (ryanbr#502)

* iOS/macOS Today: pull-to-sync — a pull now requests a strap offload, not just a UI reload (ryanbr#334)

Android ryanbr#426 added Today pull-to-sync. iOS's LiquidTodayView already had a pull
gesture (handlePull), but it only reloaded UI data (repo.refresh + load) — it
never requested a fresh strap history sync. Wire ble.syncNow() into that existing
refresh action so a pull kicks a manual offload too, matching Android.

syncNow() is internally gated (connected + bonded + not-already-backfilling), so
a pull while disconnected or mid-offload safely no-ops — the same guard Android
routes through WhoopBleClient.canRequestSync.

Observe BLEManager (injected at the app roots beside model), NOT AppModel: AppModel
@publishes bpm on the ~1 Hz HR tick, so observing it would re-render all of Today
every second (the exact churn the LiveState leaves isolate). BLEManager only
publishes connect/discovery state, never HR.

App-target Swift (shared iOS + macOS); validated via app-build.

* Android Today pull-to-sync: fix stale enabled-gate (re-review of ryanbr#426)

material3 1.2.1's rememberPullToRefreshState captures the enabled lambda ONCE
(rememberSaveable, no rememberUpdatedState), so { canPullToSync } froze the plain
Boolean from the FIRST composition. Today usually first composes before the strap
has (re)connected, so the gesture stayed disabled for the whole session and
pull-to-sync (ryanbr#426) silently never worked in the common cold-launch flow.

Read the stable liveSnap State live inside the enabled lambda instead, so each
gesture check sees the current connected/bonded/backfilling. Safe either way
(syncNow is triple-gated); this just lets the gesture enable once the strap is ready.

compileFullDebugKotlin + TodayExplainabilityTest green.
simoncad7 pushed a commit to simoncad7/noop that referenced this pull request Aug 17, 2026
Co-authored-by: kavemang <kavemang@users.noreply.github.com>
simoncad7 pushed a commit to simoncad7/noop that referenced this pull request Aug 17, 2026
…enabled-gate (ryanbr#502)

* iOS/macOS Today: pull-to-sync — a pull now requests a strap offload, not just a UI reload (ryanbr#334)

Android ryanbr#426 added Today pull-to-sync. iOS's LiquidTodayView already had a pull
gesture (handlePull), but it only reloaded UI data (repo.refresh + load) — it
never requested a fresh strap history sync. Wire ble.syncNow() into that existing
refresh action so a pull kicks a manual offload too, matching Android.

syncNow() is internally gated (connected + bonded + not-already-backfilling), so
a pull while disconnected or mid-offload safely no-ops — the same guard Android
routes through WhoopBleClient.canRequestSync.

Observe BLEManager (injected at the app roots beside model), NOT AppModel: AppModel
@publishes bpm on the ~1 Hz HR tick, so observing it would re-render all of Today
every second (the exact churn the LiveState leaves isolate). BLEManager only
publishes connect/discovery state, never HR.

App-target Swift (shared iOS + macOS); validated via app-build.

* Android Today pull-to-sync: fix stale enabled-gate (re-review of ryanbr#426)

material3 1.2.1's rememberPullToRefreshState captures the enabled lambda ONCE
(rememberSaveable, no rememberUpdatedState), so { canPullToSync } froze the plain
Boolean from the FIRST composition. Today usually first composes before the strap
has (re)connected, so the gesture stayed disabled for the whole session and
pull-to-sync (ryanbr#426) silently never worked in the common cold-launch flow.

Read the stable liveSnap State live inside the enabled lambda instead, so each
gesture check sees the current connected/bonded/backfilling. Safe either way
(syncNow is triple-gated); this just lets the gesture enable once the strap is ready.

compileFullDebugKotlin + TodayExplainabilityTest green.
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.

Today: pull-to-refresh to trigger a strap sync (pairs with the #258 sync chip)

2 participants