Skip to content

Fitness Age / Vitality permanently "not ready" on live-BLE straps (#349) - #359

Merged
ryanbr merged 1 commit into
mainfrom
fix/fitness-age-active-strap-id
Jul 13, 2026
Merged

ryanbr merged 1 commit into
mainfrom
fix/fitness-age-active-strap-id

Conversation

@ryanbr

@ryanbr ryanbr commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Fixes #349.

The bug

Fitness Age / Vitality / Body Age stay permanently on the "not ready" checklist (every item green), and the Vitality/Body Age tile shows No Data, for anyone paired over live Bluetooth. Tapping refresh even toasts "Fitness Age updated." — because recomputeFitnessAgeOnly genuinely did write; the screen just never reads the source it wrote to.

Excellent diagnosis in the issue (DB proof + exact lines), confirmed against the code:

  • IntelligenceEngine writes the weekly computed scores under "<activeStrapId>-noop".
  • HealthScreen (COMPUTED_SOURCE) and the Today "Your cards" tiles read a hardcoded "my-whoop-noop".
  • Import installs match by luck (activeStrapId == "my-whoop"). A live-BLE strap's registry id is "whoop-<mac>", so the engine writes "whoop-<mac>-noop" and every read misses it.

The fix

Read the computed series through the active-strap union — the active strap's "-noop" sibling + the canonical "my-whoop-noop", deduped per day with the active strap winning:

suspend fun metricSeriesComputedUnion(activeStrapId, key, from, to): List<MetricSeriesRow>

This is the same resolver (computedSourceIds) every other read already uses, and mirrors the layering iOS's Repository.exploreSeries already does — so iOS was never affected; this brings Android back in line. Import users (activeStrapId == "my-whoop") collapse to the single canonical id, byte-identical to before.

Scope — the report cited HealthScreen; the Today card had it too

Grepping the whole bug class (hardcoded "my-whoop-noop" reads) surfaced the same bug in the pinned Today cards, so both are fixed:

  • HealthScreen — 6 read sites (fitness_age / vo2max_est / vitality / body_age + the two vital-detail tables). Removed the now-dead COMPUTED_SOURCE constant.
  • TodayScreen — the pinned Fitness age / Vitality cards.
  • (FusionTypes.NOOP_COMPUTED is an arbitration label, not a read source; AndroidDiagnostics already unions — both left as-is.)

Verification

  • New pure companion mergeComputedSeriesUnion + ResolverUnionTest cases (active strap wins per shared day, canonical fills gaps, single-source passthrough).
  • compileFullDebugKotlin clean; full testFullDebugUnitTest suite green.
  • Android-only; no app-target Swift touched.

A live-BLE 4.0 reporter should confirm on-device, but the resolver + tests prove the id now resolves.

… hardcoded id

Fitness Age / Vitality / Body Age stayed permanently "not ready" (tiles showed
"No Data") for anyone paired over live Bluetooth (#349). IntelligenceEngine
writes the weekly computed scores under "<activeStrapId>-noop", but HealthScreen
(COMPUTED_SOURCE) and the Today "Your cards" tiles read a hardcoded
"my-whoop-noop". Import installs match by luck (activeStrapId == "my-whoop"); a
live-BLE strap writes "whoop-<mac>-noop", so every read missed it — and the
refresh toast lied ("Fitness Age updated." while the card never re-read the
source it had just written).

Read the computed metricSeries through the active-strap UNION instead
(WhoopRepository.metricSeriesComputedUnion): the active strap's "-noop" sibling +
the canonical "my-whoop-noop", deduped per day with the active strap winning —
the same resolver every other read already uses, and the layering iOS's
Repository.exploreSeries already does (iOS was unaffected). Import users collapse
to the single canonical id, byte-identical to before.

- HealthScreen: 6 read sites (fitness_age / vo2max_est / vitality / body_age +
  the two vital-detail tables).
- TodayScreen: the pinned Fitness age / Vitality cards had the same hardcoded
  read — fixed too (the issue only cited HealthScreen).
- New pure companion mergeComputedSeriesUnion + ResolverUnionTest coverage.

Android-only; iOS already resolves the computed union.
@ryanbr
ryanbr merged commit e72e7f4 into main Jul 13, 2026
@ryanbr
ryanbr deleted the fix/fitness-age-active-strap-id branch July 13, 2026 01:30
ryanbr added a commit that referenced this pull request Jul 14, 2026
…s no longer export an empty zip (#458) (#468)

Android (the reported bug, verified at HEAD): exportZip defaulted
deviceId="my-whoop", but a live-BLE install's engine banks computed
scores under "<strapId>-noop" - every read hit ids owning zero rows and
the toast said 'Exported 0 days, 0 sleeps, 0 journal entries' while the
app displayed months of history. It survived the #359 sweep because that
grep targeted the hardcoded "my-whoop-noop" string, not DEFAULT
PARAMETERS.

- The default is REMOVED (compiler-enforced decision at every call site);
  Settings threads the registry's active strap id.
- Every read resolves through the #814 active-union helpers:
  daysMerged/sleepSessionsMerged already did once handed the active id;
  workouts move to workoutsUnion/detectedWorkoutsUnion; the imported-days
  set, journal, cycles metricSeries and the sidecar union inline with
  active-first dedup on each row's natural key ((day), (day, question),
  (startTs, sport)).
- New reads are a strict SUPERSET of the old ones: nothing that previously
  exported can be lost, and a single-canonical install collapses to one id
  per side, byte-identical to before.

Apple (mirror-image of the same class): CsvExport read repo.deviceId (the
ACTIVE id, #814) plus its -noop sibling only, so a strap remove+re-add
orphaned the canonical "my-whoop" rows (prior CSV import + canonical
engine history) from the export. Reads now union
importedReadIds/computedReadIds (de-privatized, same module) with the
same active-first natural-key dedups; workoutSource takes the computed-id
LIST since a detected row can carry either union id.

NOT in this PR (separate concern): the sibling audit of the 7 remaining
'= "my-whoop"' default parameters on WhoopRepository methods.

Verification: Android compileFullDebugKotlin + testFullDebugUnitTest =
2709 tests, 0 failures (union resolvers pinned by ResolverUnionTest; no
Robolectric/in-memory Room in the JVM suite for an exportZip repro).
Swift is app-target: on-demand app-build gates the merge. Reporter
(MasterSMax12) has offered on-device verification on the live-BLE 4.0
setup that confirmed #349/#359.
DX23876 pushed a commit to DX23876/noop that referenced this pull request Jul 22, 2026
… hardcoded id (ryanbr#359)

Fitness Age / Vitality / Body Age stayed permanently "not ready" (tiles showed
"No Data") for anyone paired over live Bluetooth (ryanbr#349). IntelligenceEngine
writes the weekly computed scores under "<activeStrapId>-noop", but HealthScreen
(COMPUTED_SOURCE) and the Today "Your cards" tiles read a hardcoded
"my-whoop-noop". Import installs match by luck (activeStrapId == "my-whoop"); a
live-BLE strap writes "whoop-<mac>-noop", so every read missed it — and the
refresh toast lied ("Fitness Age updated." while the card never re-read the
source it had just written).

Read the computed metricSeries through the active-strap UNION instead
(WhoopRepository.metricSeriesComputedUnion): the active strap's "-noop" sibling +
the canonical "my-whoop-noop", deduped per day with the active strap winning —
the same resolver every other read already uses, and the layering iOS's
Repository.exploreSeries already does (iOS was unaffected). Import users collapse
to the single canonical id, byte-identical to before.

- HealthScreen: 6 read sites (fitness_age / vo2max_est / vitality / body_age +
  the two vital-detail tables).
- TodayScreen: the pinned Fitness age / Vitality cards had the same hardcoded
  read — fixed too (the issue only cited HealthScreen).
- New pure companion mergeComputedSeriesUnion + ResolverUnionTest coverage.

Android-only; iOS already resolves the computed union.
DX23876 pushed a commit to DX23876/noop that referenced this pull request Jul 22, 2026
…s no longer export an empty zip (ryanbr#458) (ryanbr#468)

Android (the reported bug, verified at HEAD): exportZip defaulted
deviceId="my-whoop", but a live-BLE install's engine banks computed
scores under "<strapId>-noop" - every read hit ids owning zero rows and
the toast said 'Exported 0 days, 0 sleeps, 0 journal entries' while the
app displayed months of history. It survived the ryanbr#359 sweep because that
grep targeted the hardcoded "my-whoop-noop" string, not DEFAULT
PARAMETERS.

- The default is REMOVED (compiler-enforced decision at every call site);
  Settings threads the registry's active strap id.
- Every read resolves through the ryanbr#814 active-union helpers:
  daysMerged/sleepSessionsMerged already did once handed the active id;
  workouts move to workoutsUnion/detectedWorkoutsUnion; the imported-days
  set, journal, cycles metricSeries and the sidecar union inline with
  active-first dedup on each row's natural key ((day), (day, question),
  (startTs, sport)).
- New reads are a strict SUPERSET of the old ones: nothing that previously
  exported can be lost, and a single-canonical install collapses to one id
  per side, byte-identical to before.

Apple (mirror-image of the same class): CsvExport read repo.deviceId (the
ACTIVE id, ryanbr#814) plus its -noop sibling only, so a strap remove+re-add
orphaned the canonical "my-whoop" rows (prior CSV import + canonical
engine history) from the export. Reads now union
importedReadIds/computedReadIds (de-privatized, same module) with the
same active-first natural-key dedups; workoutSource takes the computed-id
LIST since a detected row can carry either union id.

NOT in this PR (separate concern): the sibling audit of the 7 remaining
'= "my-whoop"' default parameters on WhoopRepository methods.

Verification: Android compileFullDebugKotlin + testFullDebugUnitTest =
2709 tests, 0 failures (union resolvers pinned by ResolverUnionTest; no
Robolectric/in-memory Room in the JVM suite for an exportZip repro).
Swift is app-target: on-demand app-build gates the merge. Reporter
(MasterSMax12) has offered on-device verification on the live-BLE 4.0
setup that confirmed ryanbr#349/ryanbr#359.
simoncad7 pushed a commit to simoncad7/noop that referenced this pull request Aug 17, 2026
… hardcoded id (ryanbr#359)

Fitness Age / Vitality / Body Age stayed permanently "not ready" (tiles showed
"No Data") for anyone paired over live Bluetooth (ryanbr#349). IntelligenceEngine
writes the weekly computed scores under "<activeStrapId>-noop", but HealthScreen
(COMPUTED_SOURCE) and the Today "Your cards" tiles read a hardcoded
"my-whoop-noop". Import installs match by luck (activeStrapId == "my-whoop"); a
live-BLE strap writes "whoop-<mac>-noop", so every read missed it — and the
refresh toast lied ("Fitness Age updated." while the card never re-read the
source it had just written).

Read the computed metricSeries through the active-strap UNION instead
(WhoopRepository.metricSeriesComputedUnion): the active strap's "-noop" sibling +
the canonical "my-whoop-noop", deduped per day with the active strap winning —
the same resolver every other read already uses, and the layering iOS's
Repository.exploreSeries already does (iOS was unaffected). Import users collapse
to the single canonical id, byte-identical to before.

- HealthScreen: 6 read sites (fitness_age / vo2max_est / vitality / body_age +
  the two vital-detail tables).
- TodayScreen: the pinned Fitness age / Vitality cards had the same hardcoded
  read — fixed too (the issue only cited HealthScreen).
- New pure companion mergeComputedSeriesUnion + ResolverUnionTest coverage.

Android-only; iOS already resolves the computed union.
simoncad7 pushed a commit to simoncad7/noop that referenced this pull request Aug 17, 2026
…s no longer export an empty zip (ryanbr#458) (ryanbr#468)

Android (the reported bug, verified at HEAD): exportZip defaulted
deviceId="my-whoop", but a live-BLE install's engine banks computed
scores under "<strapId>-noop" - every read hit ids owning zero rows and
the toast said 'Exported 0 days, 0 sleeps, 0 journal entries' while the
app displayed months of history. It survived the ryanbr#359 sweep because that
grep targeted the hardcoded "my-whoop-noop" string, not DEFAULT
PARAMETERS.

- The default is REMOVED (compiler-enforced decision at every call site);
  Settings threads the registry's active strap id.
- Every read resolves through the ryanbr#814 active-union helpers:
  daysMerged/sleepSessionsMerged already did once handed the active id;
  workouts move to workoutsUnion/detectedWorkoutsUnion; the imported-days
  set, journal, cycles metricSeries and the sidecar union inline with
  active-first dedup on each row's natural key ((day), (day, question),
  (startTs, sport)).
- New reads are a strict SUPERSET of the old ones: nothing that previously
  exported can be lost, and a single-canonical install collapses to one id
  per side, byte-identical to before.

Apple (mirror-image of the same class): CsvExport read repo.deviceId (the
ACTIVE id, ryanbr#814) plus its -noop sibling only, so a strap remove+re-add
orphaned the canonical "my-whoop" rows (prior CSV import + canonical
engine history) from the export. Reads now union
importedReadIds/computedReadIds (de-privatized, same module) with the
same active-first natural-key dedups; workoutSource takes the computed-id
LIST since a detected row can carry either union id.

NOT in this PR (separate concern): the sibling audit of the 7 remaining
'= "my-whoop"' default parameters on WhoopRepository methods.

Verification: Android compileFullDebugKotlin + testFullDebugUnitTest =
2709 tests, 0 failures (union resolvers pinned by ResolverUnionTest; no
Robolectric/in-memory Room in the JVM suite for an exportZip repro).
Swift is app-target: on-demand app-build gates the merge. Reporter
(MasterSMax12) has offered on-device verification on the live-BLE 4.0
setup that confirmed ryanbr#349/ryanbr#359.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant