Repository navigation
Fitness Age: add a "refresh" button to force an immediate recompute - #142
Merged
Merged
Conversation
Tapping the Today Fitness Age card when it reads "No Data" led to a generic "Not enough history yet -- needs two readings to chart it" (Android) / "Import your history first" (iOS). Both are wrong: Fitness Age is COMPUTED on-device from resting HR + recent activity, and the real gate is >=4 of the last 7 nights with a resting-HR reading (age/sex default, so they're rarely the blocker). Neither message told the user what it actually needs, and the helpful readiness countdown that #81 added lived only on the Health hub, a screen the card doesn't open. Now the card's tap-through, when there's no weekly value yet, shows the same readiness checklist + concrete "N more nights of wear and we can show your Fitness Age" countdown the hub does. Android: VitalDetailScreen renders the readiness card for key "fitness_age" with zero points (a single reading keeps the generic "needs two to chart" note -- the value already shows). Extracted rememberFitnessReadiness so the hub and the card share ONE gate (no drift). Subtitle reflects the not-ready state. iOS: MetricDetailView shows the countdown for the fitness_age empty state instead of the misleading "import your history" copy. Lifted fitnessReadyLead out of FitnessAgeCard into a file-scope fitnessReadyLeadCopy(rhrDays:hasAge:hasSex:) shared by both surfaces (still word-for-word matched to Android).
…-pass rows
User report: readiness card shows every input satisfied ("Resting heart
rate: 7 of last 7 nights", age/sex Set), yet Fitness Age stays "No Data"
while Vitality/Body Age computes. Root cause: the engine gated Fitness
Age on `fa7 = dailies` -- THIS recompute pass's freshly scored nights
(a recompute only re-scores nights whose raw HR still lives in the DB) --
while the readiness card counts the persisted/merged daily history. So
the two disagree: the card promises "ready", the engine sees <4 RHR
nights and never computes. Vitality survives because it needs only 3 of
ANY input (sleep/HRV/steps), so a sparse pass still clears its bar.
Fix: gate + compute Fitness Age from the SAME merged last-7 the card and
dashboard read (repo.daysMerged / repo.dailyMetrics union), kept separate
from `fa7` so Vitality is untouched. Monotonic-safe: the merged view can
only yield >= the RHR nights `dailies` had, using valid values, so it can
never make Fitness Age compute less often.
Also adds a strap-log line (Android) recording the RHR-night count the
engine actually sees for the gate, so a divergence from the card's "N of
last 7" is provable from an export.
Both platforms (Android IntelligenceEngine.kt + iOS IntelligenceEngine.swift).
…scores Follow-up after "original noop worked" + the readiness card showing 7/7: the root cause is the engine gating on computed-only `dailies`, while the user's resting HR comes (partly) from IMPORTED daily aggregates (Apple Health / Health Connect) that only the MERGED view carries. The first cut read the merged view AFTER the recompute's own delete/evict, so it could still see the narrowed window (a potential no-op) and the Android read was unbounded. Now snapshot the persisted/merged history BEFORE the rewrite and gate on its UNION with this pass's fresh scores (by day, fresh wins): an RHR night counts whether it survives in the store, was just scored, or came from an import. Windowed to the recompute range (fixes the unbounded-read nit). Vitality stays on `dailies` (already computes) , untouched. Android + iOS.
…age-refresh-button
The weekly Fitness Age is only written by the intelligence recompute (15-min backstop, gated by an HR-fingerprint watermark), so a ready user could wait a while to see it. Add a small refresh affordance on the not-ready readiness card that recomputes Fitness Age NOW from the PERSISTED merged daily history , no raw-HR rescoring , so it returns fast, works offline, and reads exactly what the readiness card shows (it also sidesteps the recompute's own delete-timing, so it lands the value even where the auto-pass might not). Shared gate: extracted the #140 gate + compute into IntelligenceEngine. fitnessAgeRows(...), used by BOTH the recompute pass and the new recomputeFitnessAgeOnly(...) so the two can never drift. Android: AppViewModel.refreshFitnessAgeNow + a Refresh IconButton (spinner while running) on FitnessReadinessCard, wired on the Health hub (FitnessAgeSection) and the Today-card tap-through (VitalDetailScreen), re-reading the value on completion + a Toast. iOS: engine parity (fitnessAgeRows / recomputeFitnessAgeOnly), a refresh button on ReadinessChecklistCard (HealthView) and the fitness_age empty state (MetricDetailView), via the app-wide IntelligenceEngine env object. Stacked on #139 (readiness surfaces) + #140 (gate fix), which this includes.
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
…tton Fitness Age: add a "refresh" button to force an immediate recompute
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.
What
A small refresh button on the Fitness Age not-ready card that forces an immediate recompute — so a ready user doesn't wait for the weekly pass.
NOOP writes Fitness Age only during the intelligence recompute (a 15-min backstop gated by an HR-fingerprint watermark), so the value can lag. This button recomputes Fitness Age now, from the persisted merged daily history — no raw-HR rescoring — so it:
How
Shared gate (no drift): extracted #140's gate + compute into
IntelligenceEngine.fitnessAgeRows(...), used by both the recompute pass and the newrecomputeFitnessAgeOnly(...).Android:
AppViewModel.refreshFitnessAgeNow→ aRefreshIconButton(spinner while running) onFitnessReadinessCard, wired on the Health hub (FitnessAgeSection) and the Today-card tap-through (VitalDetailScreen); re-reads the value on completion + a Toast.iOS: engine parity (
fitnessAgeRows/recomputeFitnessAgeOnly); anarrow.clockwisebutton onReadinessChecklistCard(HealthView) and thefitness_ageempty state (MetricDetailView), via the app-wideIntelligenceEngineenvironment object (preview injections added).Notes
MetricDetailViewshows the value hero; AndroidVitalDetailScreen(a trend view) shows "not enough history to chart" until a 2nd weekly reading — the Toast + the Health-hub hero carry the confirmation either way.Verification
:app:compileFullDebugKotlinpasses locally.medianOf/saturdayKey; profile passed to the shared helper as primitives (no cross-actor read); required@EnvironmentObject IntelligenceEngineis app-wide injected with both direct previews updated.