Repository navigation
Description
Surface "no new nights for N days" instead of silently falling back to calibrating
The problem
When a strap stops delivering nightly data — but still connects — NOOP gives no signal that
anything is wrong. It keeps showing "connected", and the Charge/HRV baseline quietly ages out and
returns to the calibrating/building state. From the outside this is indistinguishable from a data
loss, and it is easy to blame whatever happened most recently (an app update, a reinstall).
Concrete case, WHOOP 5.0 on iOS 9.0.2. The database is fully intact:
[display] dataVolume dbRows=1649044 importedDays=332 workouts=484
The bond is intact — the 5/MG 0x2A19 read only succeeds post-bond (#490,
BLEManager.swift), and it returns values throughout the session. What is missing is the data:
no live-HR notification arrives, and the offload comes back empty:
Backfill: session ended — reason=timeout
[connection] offload result=stalled (idle timeout, rows=0 so far)
Backfill: WHOOP 5/MG offload empty 2× — history sync is experimental on 5.0; ...
So every surface reads "connected" — correctly — while nothing new is actually landing. (The
underlying cause in this particular case looks like the CoreBluetooth state-restoration path; that
is filed separately. This issue is about the reporting, which would be equally misleading for any
other cause.)
Once that state persists past Baselines.staleDays (14), computeStatus flips the baseline to
.stale, and with too few valid nights in the window it reads .calibrating again — the user sees
their scores reset to "collecting nights" with no explanation, up to two weeks after the actual
cause. The delay is what makes it so misleading: it lands on whatever the user did that week.
The idea
Surface the age of the newest nightly value wherever the calibrating/building state is shown:
- On Today / the Charge card, when the baseline is
.staleor back to.calibratingand the
newest night is older thanstaleDays, say so directly — e.g. "No new nights from your strap for
14 days" — rather than only "building your baseline". - When the strap is connected but the last N offloads returned 0 rows, and no live-HR notification
has arrived this session, distinguish "connected" from "connected, but no data": the app already
knows both facts (the offload result isstalled … rows=0; no HR notification has been parsed). - Point at the actionable causes — the single-bond limit documented in the README ("Pairing a
WHOOP 5.0 / MG — read this first") is the most common one, and a one-line hint linking that
section would resolve many of these reports without a log.
This is diagnostics/UX only — no change to the analytics, no change to what is stored, and no claim
that 5/MG history sync should work. The point is that the honest calibrating state is correct but
currently arrives without its reason attached.
Alternatives you've considered
- Doing nothing and relying on the strap log: it works, but only for users who know to export it,
and the relevant signal is an absent line, which is hard to spot. - Warning on every empty offload: too noisy — a single empty offload is normal on 5/MG. Keying the
message to the age of the newest stored night avoids false alarms.
Additional context
Baselines.staleDays = 14,minNightsSeed = 4,minNightsTrust = 14
(Packages/StrandAnalytics/Sources/StrandAnalytics/Baselines.swift), consumed by
computeStatus(nValid:nightsSinceUpdate:).- Related but distinct: Sync indicator missing (or I am dumb?) #245 (no sync indicator) asks for progress during a sync; this asks for a
signal when syncs keep completing with nothing in them. - Cross-platform: the same
staleDaysgate exists in the Kotlin twin, so the Android surface would
need the matching treatment.
Activity
digitalerdude commentedon Jul 19, 2026
Corrected the body: the original write-up claimed the strap never completed its bond. That was wrong — on a 5/MG the
0x2A19battery read only succeeds post-bond (#490), and it returns values throughout the session in question, so the encrypted link was intact the whole time. The absentCLIENT_HELLO ackedline was a red herring.That does not change what this issue asks for: the strap was connected and bonded, no new nights were landing, and the baseline aged out to
calibratingwith no explanation. If anything it strengthens the case — "connected and bonded" is exactly the state in which the current UI is most misleading.The specific cause found in that session (the CoreBluetooth state-restoration path leaving the link half-initialised) is filed separately.
digitalerdude commentedon Jul 19, 2026
The restore-path cause is now filed as #613.
Surface "no new nights for N days" on the calibrating Charge card (#612…
Surface "no new nights for N days" on the calibrating Charge card (ry…
Add the missing String Catalog entries for connectedNoData (ryanbr#612 …
Distinguish "connected, no data" from "not connected" (#612) (#826)
Localize the Android recording chip, which shipped English to every l…
Localize the Android score-state note, and make its countdown a real …
Localize the Android score-state note, and make its countdown a real …
Surface "no new nights for N days" on the calibrating Charge card (ry…
Distinguish "connected, no data" from "not connected" (ryanbr#612) (r…
Localize the Android recording chip, which shipped English to every l…
Localize the Android score-state note, and make its countdown a real …
Say WHY the count is not moving, not just what it counts
today: the calibrating ring counts nights, and says when they arrive …