Fitness Age: "N more nights" countdown while it's building (iOS + Android)#81
Merged
Conversation
The Fitness Age card sat on a vague "A few more days of wear…" until it had enough history (resting HR on ≥4 of the last 7 nights). Give it a concrete countdown instead, mirroring the honest "Calibrating - N of 4 nights" treatment Charge already uses (#335). - Shared engine helper FitnessAgeEngine.nightsUntilReady(rhrDays) = max(0, minCoverageDays - rhrDays), on BOTH platforms so the number is identical. - The not-ready lead becomes "N more nights of wear and we can show your Fitness Age." (singular/plural handled), noting age/sex only when those are actually missing. Copy is word-for-word identical between iOS (HealthView) and Android (HealthScreen). - Swift test for the countdown; Android compiles locally. UI + one pure helper; no scoring change. rhrDays and the floor were already in the readiness path.
This was referenced Jul 9, 2026
ryanbr
added a commit
that referenced
this pull request
Jul 9, 2026
Fix iOS/macOS build: ReadinessChecklistCard.lead is a String (#81 regression)
pipiche38
pushed a commit
to pipiche38/noop
that referenced
this pull request
Jul 9, 2026
…alizedStringKey The v8.5.0 release build failed to compile on both iOS and macOS: HealthView.swift:744: cannot convert 'String' to 'LocalizedStringKey' fitnessReadyLead() (ryanbr#81, Fitness Age countdown) returns an already-localized String (String(localized:), word-for-word matched to Android), but the card's `lead` was typed LocalizedStringKey?. Re-keying an already-resolved string is wrong anyway; the fix is to type `lead` as String? and render it verbatim via Text(String). Both callers already pass a String / nil, and the only use is the single Text(lead), so this is self-contained. This slipped in because the fork runs the Swift build only at release/on-demand, not per-commit, so ryanbr#81's iOS-only change wasn't compiled before merge.
tanarchytan
pushed a commit
to tanarchytan/noop
that referenced
this pull request
Jul 9, 2026
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 ryanbr#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).
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.
Requested: a countdown for how long until the Fitness Age generates. Today the card shows a vague "A few more days of wear…"; this makes it a concrete "N more nights of wear and we can show your Fitness Age." — the same honest "building, not broken" treatment Charge already uses ("Calibrating - N of 4 nights", #335).
What drives it
The only thing gating the headline is resting-HR coverage:
rhrDays ≥ minCoverageDays (4). So the countdown is justmax(0, 4 − rhrDays)— both values already in the readiness path.Changes
FitnessAgeEngine.nightsUntilReady(rhrDays)on both platforms (Swift + Kotlin) so the number is identical.HealthView.FitnessAgeSection) and Android (HealthScreen.FitnessAgeSection): the not-ready lead is now the countdown, noting age/sex only when those are actually missing. The six copy variants are word-for-word identical across platforms (verified: iOS\(n)↔ Android$remaining).nightsUntilReady(0→4, 1→3, 3→1, 4→0, 7→0).Testing
compileFullDebugKotlin, exit 0) — caught + fixed a@Composable-annotation-detach on the way (re-review value).nightsUntilReadytest can't run on WSL (StrandAnalytics pulls GRDB →sqlite3.hwall), but it's trivial arithmetic and runs in CI.UI + one pure helper; no scoring change.
Files:
FitnessAgeEngine.swift/.kt,HealthView.swift,HealthScreen.kt,FitnessAgeEngineTests.swift.