Skip to content

Surface "no new nights for N days" instead of silently falling back to calibrating #612

Open

Description

@digitalerdude

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 .stale or back to .calibrating and the
    newest night is older than staleDays, 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 is stalled … 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 staleDays gate exists in the Kotlin twin, so the Android surface would
    need the matching treatment.

Activity

  1. digitalerdude commented on Jul 19, 2026

    @digitalerdude
    Author

    Corrected the body: the original write-up claimed the strap never completed its bond. That was wrong — on a 5/MG the 0x2A19 battery 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 absent CLIENT_HELLO acked line 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 calibrating with 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.

  2. digitalerdude commented on Jul 19, 2026

    @digitalerdude
    Author

    The restore-path cause is now filed as #613.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions