Skip to content

Fitness Age: "N more nights" countdown while it's building (iOS + Android)#81

Merged
ryanbr merged 1 commit into
mainfrom
fitness-age-countdown
Jul 9, 2026
Merged

Fitness Age: "N more nights" countdown while it's building (iOS + Android)#81
ryanbr merged 1 commit into
mainfrom
fitness-age-countdown

Conversation

@ryanbr

@ryanbr ryanbr commented Jul 8, 2026

Copy link
Copy Markdown
Owner

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 just max(0, 4 − rhrDays) — both values already in the readiness path.

Changes

  • Shared engine helper FitnessAgeEngine.nightsUntilReady(rhrDays) on both platforms (Swift + Kotlin) so the number is identical.
  • iOS (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).
  • Swift test for nightsUntilReady (0→4, 1→3, 3→1, 4→0, 7→0).

Testing

  • Android compiles locally (compileFullDebugKotlin, exit 0) — caught + fixed a @Composable-annotation-detach on the way (re-review value).
  • The Swift nightsUntilReady test can't run on WSL (StrandAnalytics pulls GRDB → sqlite3.h wall), 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.

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.
@ryanbr ryanbr merged commit 0e06dde into main 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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant