Skip to content

feat(sync): show sync progress as a share, a frontier time and time remaining - #2751

Open
matthiasthomas wants to merge 1 commit into
ryanbr:mainfrom
matthiasthomas:feat/sync-progress
Open

matthiasthomas wants to merge 1 commit into
ryanbr:mainfrom
matthiasthomas:feat/sync-progress

Conversation

@matthiasthomas

@matthiasthomas matthiasthomas commented Oct 9, 2026 •

Copy link
Copy Markdown

What this PR does

Shows how far a history sync has got, not just that it is moving. The Devices card and the Today sync ring could only count chunks ("Syncing… 2,247 chunks", and "2,229" in the ring), because the strap never says how many chunks a sync will take. During a long catch-up there was no way to tell whether it would take two minutes or an hour.

The decoded samples carry their own times, so progress can be measured in time instead of chunks:

  • Backfiller reports each decoded chunk's earliest and latest sample time through a new onChunkDataRange callback (default nil).
  • LiveState keeps the session's first sample time, its newest sample time (the frontier), and when the session started. All three reset with syncChunksThisSession in beginBackfill.
  • SyncProgress (pure, unit-tested) turns those into:
    • a share of the catch-up;
    • the time the sync has reached;
    • a time-remaining estimate. The strap keeps recording while it drains, so the target moves forward one second per second, and only the drain's excess speed closes the gap.
  • The estimate is withheld for the first 20 s, and whenever the drain is not clearly outrunning real time. Within 5 minutes of now it reads as caught up ("finishing up").

LiveState.syncChunksThisSession's comment says a count was used because "total pending is unknowable from the protocol". That is still true in chunks. This measures in seconds of data instead, which the protocol does give.

What changes on screen:

  • Devices → Strap history: "Syncing… 62% · synced up to 1:34 AM · about 12 min left", with a LiquidTube progress bar. It shows the chunk count until the first chunk decodes.
  • Today sync ring (ChargeSyncIndicator): the percentage replaces the chunk tally in the centre. The centre readout is now held inside the ring: one line, scaling to fit. A four-digit value no longer runs into the arc, and "100%" no longer truncates to "10…".
  • Spoken labels (ring and Devices card) carry the same summary.

Devices sync card during a catch-up

This screenshot was taken before the last tweak: at 100% the card now says "finishing up" rather than "less than a minute left".

Today ring during a catch-up, with the percentage held inside the arc (previously a four-digit chunk tally ran into the ring, and "100%" truncated to "10…"):

Sync ring at 24% Sync ring at 100%

Not changed:

  • Android: not touched; it has its own sync chip and Devices card. The calculation is a small pure function that could be ported with a twin test.
  • iOS Live Activity: SyncActivityCopy still uses the chunk count.

Type of change

  • Bug fix
  • New feature
  • Refactor / cleanup
  • Documentation
  • CI / tooling

How it was tested

  • On hardware (WHOOP 4.0, firmware 41.17.6.0, macOS 26.4.1): I quit NOOP while wearing the strap and reopened it to drain the backlog. The Devices card showed the share, the frontier time and the remaining time through the drain, and the Today ring showed the percentage (screenshots above, at 24% and at the end of the drain).
  • Unit tests: SyncProgressTests (6 tests) cover:
    • the share and time remaining against a moving target;
    • the 5-minute caught-up slack and the "finishing up" wording;
    • never showing 100% before caught up;
    • no estimate too early or when the drain is not outrunning real time;
    • inconsistent inputs;
    • the remaining-time wording.
  • Full StrandTests (macOS): 2,285 tests ran on this branch. Two failures are timezone-dependent and unrelated to this change: SkinTempAbsoluteDisplayTests and SleepCarriedStampTests fail in America/New_York and pass with TEST_RUNNER_TZ=UTC.
  • StrandDesign package: swift test runs 134 tests with 0 failures.
  • Translations: Tools/i18n_audit.py --ci upstream/main passes. There are 8 new strings, translated into de, es, fr, it, pl, pt-PT, ru, zh-Hans and zh-Hant. The minute and hour wordings use invariant abbreviations ("мин", "godz."), so no plural variants are needed.
  • Lint: Tools/doc_comment_lint.py reports no new detached doc comments.
  • Tooling tests: python3 -m unittest discover in Tools/ has 4 failures in test_check_source_references. They fail identically on an untouched upstream/main checkout on this machine.
  • Not verified locally: the iOS compile, because my Xcode lacks the watchOS platform the NOOPiOS scheme embeds. LiquidTodayView, DevicesView and the StrandDesign change are shared, so app-build.yml is the check there.

Checklist

  • Swift package tests pass for any package I touched (swift test in Packages/StrandDesign)
  • Android unit tests pass if I touched android/: n/a, android/ not touched
  • No new build warnings introduced
  • UI changes use only StrandDesign tokens — no hardcoded colors, fonts, or spacing (new ring constants live in SyncRing, next to the existing ones)
  • No hardcoded hex frame bytes; protocol facts live in the schema / decoders
  • Follows the conventions in docs/CONTRIBUTING.md
  • I did not commit generated output (Strand.xcodeproj/) or any secrets/keystores

Related issues

None found.

🤖 Generated with Claude Code

…emaining

The strap does not report how many chunks a history sync will take, so the
Devices card and the Today sync ring could only count chunks ("Syncing…
2,247 chunks", "2,229" in the ring), which says the drain is moving but not
how far it has to go.

The decoded samples carry their own times, though. The Backfiller now
reports each chunk's earliest and latest sample time; LiveState keeps the
session's first sample time, its newest (the frontier) and the session's
start. SyncProgress turns those into a share of the catch-up, the time the
sync has reached, and a time-remaining estimate that accounts for the strap
still recording while it drains (the target moves forward one second per
second, so only the drain's excess speed closes the gap).

- Devices card: "Syncing… 62% · synced up to 1:34 AM · about 12 min left"
  plus a progress tube; "finishing up" within the 5-minute caught-up slack.
- Today ring: the percentage replaces the chunk tally in the centre. The
  centre readout is now held inside the ring (one line, scales to fit), so a
  four-digit value no longer runs into the arc and "100%" no longer
  truncates to "10…".
- Spoken labels carry the same summary.
- Falls back to the chunk count until the first chunk decodes.
- Eight new strings, translated into all nine catalogue locales.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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