Repository navigation
fix(ios): pull-to-sync shows "Syncing…" and stays up for the real backfill - #590
Merged
Merged
Conversation
…kfill The liquid pull-to-refresh vessel was silent and its visibility was tied only to a local `refreshing` flag that flips false ~350ms after release (once the local repo reload finishes) — but `ble.syncNow()` kicks off a real BLE history offload that can run far longer than that. Releasing the pull made the vessel vanish while the strap was still mid-sync, with no feedback beyond the easy-to-miss header SyncStatusChip. Extract the indicator into its own LiveState-isolated leaf (same pattern as LiquidLiveHR) and add a "Syncing…" label. Visibility (`syncing`) is now `refreshing || live.backfilling`, so the indicator stays up until the actual strap sync finishes, not just the local UI reload — and it stays a leaf so a live-HR notify still doesn't re-render the rest of Today. Fixes ryanbr#587.
ryanbr
added a commit
that referenced
this pull request
Jul 19, 2026
…licker it (#587 follow-up) (#608) Follow-up to #590. That change tied the pull-to-sync vessel + "Syncing…" label to `live.backfilling`, but that flag toggles false→true between EVERY offload chunk (exitBackfilling at each HISTORY_END → auto-continue re-kick → beginBackfill), with a real BLE gap in between. A deep backlog is now up to ~24 chunks in one connection (#594 raised the auto-continue cap 6→24), so the vessel strobed in/out on every chunk boundary — the exact per-chunk toggle the MenuBar header already pins a constant height against. Debounce the visibility: a new @State `syncing` goes true instantly on syncingRaw, but goes false only after riding out a 3s quiet window with no new chunk (cancelled the moment the next chunk arrives). So the vessel stays steady through a multi-chunk drain and only drops when the sync is genuinely done. iOS/macOS shared (LiquidTodayView), one file. swiftc -parse clean; i18n audit exit 0 (reuses the existing "Syncing…" key). App-target compile to be confirmed by app-build.
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
…kfill (ryanbr#590) The liquid pull-to-refresh vessel was silent and its visibility was tied only to a local `refreshing` flag that flips false ~350ms after release (once the local repo reload finishes) — but `ble.syncNow()` kicks off a real BLE history offload that can run far longer than that. Releasing the pull made the vessel vanish while the strap was still mid-sync, with no feedback beyond the easy-to-miss header SyncStatusChip. Extract the indicator into its own LiveState-isolated leaf (same pattern as LiquidLiveHR) and add a "Syncing…" label. Visibility (`syncing`) is now `refreshing || live.backfilling`, so the indicator stays up until the actual strap sync finishes, not just the local UI reload — and it stays a leaf so a live-HR notify still doesn't re-render the rest of Today. Fixes ryanbr#587. Co-authored-by: tigercraft4 <>
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
…licker it (ryanbr#587 follow-up) (ryanbr#608) Follow-up to ryanbr#590. That change tied the pull-to-sync vessel + "Syncing…" label to `live.backfilling`, but that flag toggles false→true between EVERY offload chunk (exitBackfilling at each HISTORY_END → auto-continue re-kick → beginBackfill), with a real BLE gap in between. A deep backlog is now up to ~24 chunks in one connection (ryanbr#594 raised the auto-continue cap 6→24), so the vessel strobed in/out on every chunk boundary — the exact per-chunk toggle the MenuBar header already pins a constant height against. Debounce the visibility: a new @State `syncing` goes true instantly on syncingRaw, but goes false only after riding out a 3s quiet window with no new chunk (cancelled the moment the next chunk arrives). So the vessel stays steady through a multi-chunk drain and only drops when the sync is genuinely done. iOS/macOS shared (LiquidTodayView), one file. swiftc -parse clean; i18n audit exit 0 (reuses the existing "Syncing…" key). App-target compile to be confirmed by app-build.
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…kfill (ryanbr#590) The liquid pull-to-refresh vessel was silent and its visibility was tied only to a local `refreshing` flag that flips false ~350ms after release (once the local repo reload finishes) — but `ble.syncNow()` kicks off a real BLE history offload that can run far longer than that. Releasing the pull made the vessel vanish while the strap was still mid-sync, with no feedback beyond the easy-to-miss header SyncStatusChip. Extract the indicator into its own LiveState-isolated leaf (same pattern as LiquidLiveHR) and add a "Syncing…" label. Visibility (`syncing`) is now `refreshing || live.backfilling`, so the indicator stays up until the actual strap sync finishes, not just the local UI reload — and it stays a leaf so a live-HR notify still doesn't re-render the rest of Today. Fixes ryanbr#587. Co-authored-by: tigercraft4 <>
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…licker it (ryanbr#587 follow-up) (ryanbr#608) Follow-up to ryanbr#590. That change tied the pull-to-sync vessel + "Syncing…" label to `live.backfilling`, but that flag toggles false→true between EVERY offload chunk (exitBackfilling at each HISTORY_END → auto-continue re-kick → beginBackfill), with a real BLE gap in between. A deep backlog is now up to ~24 chunks in one connection (ryanbr#594 raised the auto-continue cap 6→24), so the vessel strobed in/out on every chunk boundary — the exact per-chunk toggle the MenuBar header already pins a constant height against. Debounce the visibility: a new @State `syncing` goes true instantly on syncingRaw, but goes false only after riding out a 3s quiet window with no new chunk (cancelled the moment the next chunk arrives). So the vessel stays steady through a multi-chunk drain and only drops when the sync is genuinely done. iOS/macOS shared (LiquidTodayView), one file. swiftc -parse clean; i18n audit exit 0 (reuses the existing "Syncing…" key). App-target compile to be confirmed by app-build.
ryanbr
added a commit
that referenced
this pull request
Sep 25, 2026
…y that Reported in #2446 by @UtkuDenizAltiok, with the cause traced and a canary to reproduce it. At launch, and on the Storage screen's Clean up, the app removed every item in the temporary directory whose name began `noop-` and was more than 60 seconds old. The comment beside it said the temp dir is NOOP's private sandbox. That is true on iOS and false on the Mac build we ship. `Strand.entitlements` does declare `com.apple.security.app-sandbox`, but entitlements apply only to a signed build, and fork-release builds with `CODE_SIGNING_ALLOWED=NO` and then ad-hoc re-signs with no entitlements at all; project.yml says as much three lines from the entitlement. So the distributed Mac app is unsandboxed, its temporary directory is the shared per-user one, and the sweep deleted other processes' files. The report lost a running `xcodebuild`'s derived data to it, from `$TMPDIR/noop-verify` and `noop-measure`. NOOP now stages its scratch in a `NOOP` folder inside the temporary directory and sweeps only what is in there. Ownership becomes a location instead of a guess about a name, so nothing outside the folder is a candidate whatever it is called. Every writer moved: the backup's settings/manifest/extract staging, the import copy, the Rhythm CSV, the export stagers, the Apple Health decompression, the Xiaomi extraction and the raw-sensor CSV. Internal scratch drops its now-redundant `noop-` prefix. The export stagers keep their filenames, because that name is what the user receives from the share sheet. Two of those live in packages the parity ledger inventories, so they spell the folder name inline rather than gaining a declaration; the ledger is unchanged, counts identical. Legacy flat scratch is still reclaimed, by EXACT prefixes this app wrote rather than by the open `noop-`. Without that, the multi-GB `noop-health-*` extraction an interrupted import strands (#590) would become permanent the moment the sweep narrowed, which is the trap in the smaller of the two fixes the report offered. The list covers only builds that predate the owned folder and can go when they are out of the field. `noop-last-crash.txt` and `noop-raw-capture-*` are deliberately NOT in that list: they are written to the caches and documents directories and were never swept. Matching a name is only safe for a directory this app stages into. The sweep now takes the directory to work in, which is also why it finally has tests: it read `FileManager.default.temporaryDirectory` directly, so there was no way to exercise it without deleting the tester's own files. The reported canaries are pinned by name. `AppModel.isNoopTempScratch` goes with it. Both its callers now ask `NoopScratch` directly, so the wrapper was left referenced by nothing; its doc had also been split across the rewiring and its first line had drifted onto the sizing function below. One thing fixed on the way past: the CSV export staged its zip under a bare UUID, which matched no `noop-` prefix, so an interrupted export leaked it. Inside the owned folder it is swept like the rest. Parity ledger unchanged, counts identical. Doc-comment lint clean. Refs #590.
ryanbr
added a commit
that referenced
this pull request
Sep 25, 2026
…y that Reported in #2446 by @UtkuDenizAltiok, with the cause traced and a canary to reproduce it. At launch, and on the Storage screen's Clean up, the app removed every item in the temporary directory whose name began `noop-` and was more than 60 seconds old. The comment beside it said the temp dir is NOOP's private sandbox. That is true on iOS and false on the Mac build we ship. `Strand.entitlements` does declare `com.apple.security.app-sandbox`, but entitlements apply only to a signed build, and fork-release builds with `CODE_SIGNING_ALLOWED=NO` and then ad-hoc re-signs with no entitlements at all; project.yml says as much three lines from the entitlement. So the distributed Mac app is unsandboxed, its temporary directory is the shared per-user one, and the sweep deleted other processes' files. The report lost a running `xcodebuild`'s derived data to it, from `$TMPDIR/noop-verify` and `noop-measure`. NOOP now stages its scratch in a folder it owns inside the temporary directory and sweeps only what is in there. Ownership becomes a location instead of a guess about a name, so nothing outside the folder is a candidate whatever it is called. The folder is named for the running bundle, not "NOOP". Everything inside it is removed without being matched by name, on the grounds that NOOP put it there, so the name has to be one nothing else will have taken, and a short one is not: macOS filesystems are case-insensitive by default, so `NOOP` and `noop` are the same directory, and the report behind this change had a process writing `noop-verify` and `noop-measure` into that same shared folder. Adopting a stranger's directory and then emptying it is this bug one level down. A bundle identifier is per-install and fork-specific. Every writer moved: the backup's settings/manifest/extract staging, the import copy, the Rhythm CSV, the export stagers, the Apple Health decompression, the Xiaomi extraction and the raw-sensor CSV. Internal scratch drops its now-redundant `noop-` prefix. The export stagers keep their filenames, because that name is what the user receives from the share sheet. Three of those live in packages the parity ledger inventories, so they spell the folder name inline rather than gaining a declaration; the ledger is unchanged, counts identical. A test pins the three spellings to the app's own, because three independent spellings of one name drift, and a drift here is not cosmetic: a writer staging outside the swept folder leaks forever, and one staging into a folder it does not own hands its files to somebody else's sweep. Legacy flat scratch is still reclaimed, by EXACT prefixes this app wrote rather than by the open `noop-`. Without that, the multi-GB `noop-health-*` extraction an interrupted import strands (#590) would become permanent the moment the sweep narrowed, which is the trap in the smaller of the two fixes the report offered. The list covers only builds that predate the owned folder and can go when they are out of the field. `noop-last-crash.txt` and `noop-raw-capture-*` are deliberately NOT in that list: they are written to the caches and documents directories and were never swept. Matching a name is only safe for a directory this app stages into. The sweep now takes the directory to work in, which is also why it finally has tests: it read `FileManager.default.temporaryDirectory` directly, so there was no way to exercise it without deleting the tester's own files. The reported canaries are pinned by name. `AppModel.isNoopTempScratch` goes with it. Both its callers now ask `NoopScratch` directly, so the wrapper was left referenced by nothing; its doc had also been split across the rewiring and its first line had drifted onto the sizing function below. One thing fixed on the way past: the CSV export staged its zip under a bare UUID, which matched no `noop-` prefix, so an interrupted export leaked it. Inside the owned folder it is swept like the rest. Parity ledger unchanged, counts identical. Doc-comment lint clean. Refs #590.
ryanbr
added a commit
that referenced
this pull request
Sep 25, 2026
…y that (#2453) Reported in #2446 by @UtkuDenizAltiok, with the cause traced and a canary to reproduce it. At launch, and on the Storage screen's Clean up, the app removed every item in the temporary directory whose name began `noop-` and was more than 60 seconds old. The comment beside it said the temp dir is NOOP's private sandbox. That is true on iOS and false on the Mac build we ship. `Strand.entitlements` does declare `com.apple.security.app-sandbox`, but entitlements apply only to a signed build, and fork-release builds with `CODE_SIGNING_ALLOWED=NO` and then ad-hoc re-signs with no entitlements at all; project.yml says as much three lines from the entitlement. So the distributed Mac app is unsandboxed, its temporary directory is the shared per-user one, and the sweep deleted other processes' files. The report lost a running `xcodebuild`'s derived data to it, from `$TMPDIR/noop-verify` and `noop-measure`. NOOP now stages its scratch in a folder it owns inside the temporary directory and sweeps only what is in there. Ownership becomes a location instead of a guess about a name, so nothing outside the folder is a candidate whatever it is called. The folder is named for the running bundle, not "NOOP". Everything inside it is removed without being matched by name, on the grounds that NOOP put it there, so the name has to be one nothing else will have taken, and a short one is not: macOS filesystems are case-insensitive by default, so `NOOP` and `noop` are the same directory, and the report behind this change had a process writing `noop-verify` and `noop-measure` into that same shared folder. Adopting a stranger's directory and then emptying it is this bug one level down. A bundle identifier is per-install and fork-specific. Every writer moved: the backup's settings/manifest/extract staging, the import copy, the Rhythm CSV, the export stagers, the Apple Health decompression, the Xiaomi extraction and the raw-sensor CSV. Internal scratch drops its now-redundant `noop-` prefix. The export stagers keep their filenames, because that name is what the user receives from the share sheet. Three of those live in packages the parity ledger inventories, so they spell the folder name inline rather than gaining a declaration; the ledger is unchanged, counts identical. A test pins the three spellings to the app's own, because three independent spellings of one name drift, and a drift here is not cosmetic: a writer staging outside the swept folder leaks forever, and one staging into a folder it does not own hands its files to somebody else's sweep. Legacy flat scratch is still reclaimed, by EXACT prefixes this app wrote rather than by the open `noop-`. Without that, the multi-GB `noop-health-*` extraction an interrupted import strands (#590) would become permanent the moment the sweep narrowed, which is the trap in the smaller of the two fixes the report offered. The list covers only builds that predate the owned folder and can go when they are out of the field. `noop-last-crash.txt` and `noop-raw-capture-*` are deliberately NOT in that list: they are written to the caches and documents directories and were never swept. Matching a name is only safe for a directory this app stages into. The sweep now takes the directory to work in, which is also why it finally has tests: it read `FileManager.default.temporaryDirectory` directly, so there was no way to exercise it without deleting the tester's own files. The reported canaries are pinned by name. `AppModel.isNoopTempScratch` goes with it. Both its callers now ask `NoopScratch` directly, so the wrapper was left referenced by nothing; its doc had also been split across the rewiring and its first line had drifted onto the sizing function below. One thing fixed on the way past: the CSV export staged its zip under a bare UUID, which matched no `noop-` prefix, so an interrupted export leaked it. Inside the owned folder it is swept like the rest. Parity ledger unchanged, counts identical. Doc-comment lint clean. Refs #590.
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.
Fixes #587.
Problem
LiquidTodayView's pull-to-refresh vessel (#334/#426/#502) is silent — no text — and its visibility is tied only to a localrefreshingflag:refreshingflips false ~350ms after release. ButsyncNow()starts a real BLE history offload that can run far longer than that. Releasing the pull makes the vessel disappear while the strap is still mid-sync, with no feedback beyond the header'sSyncStatusChip(#258) — easy to miss and not visually connected to the gesture the user just performed.Fix
liquidRefreshIndicatorinto its ownLiquidRefreshIndicatorleaf, following the file's existingLiveState-isolation convention (mirrorsLiquidLiveHR) — it owns@EnvironmentObject var live: LiveStateitself so a ~1 Hz live-HR notify still doesn't re-render the rest of Today.syncing) is nowrefreshing || live.backfillinginstead of justrefreshing— the indicator (vessel + label) stays up until the actual strap backfill finishes, not just the local UI reload.Scope
iOS only (
Strand/Liquid/LiquidTodayView.swift), one file. No BLE/protocol/schema change — reads the existingLiveState.backfillingflag the header chip already uses. Android isn't affected (different pull-to-sync implementation,PullToRefreshContainerinTodayScreen.kt, already fixed for its own visibility bug in #583).Testing
macOS
Strandscheme builds clean (0 errors) —xcodebuild -scheme Strand -destination 'platform=macOS'. FullNOOPiOSscheme not run locally (missing watchOS SDK in my environment, same constraint as prior PRs); this file is shared between iOS and macOS Today (liquidTodayEnabledgate), so the macOS build exercises the same code path.No behavior change for the non-syncing states (pull-in-progress, idle) — only the syncing branch's visibility condition and content changed.