Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b7eecd2
feat(ble): add Polar H10 telemetry models and protocol byte parsers
StuGotz Oct 20, 2026
9212b47
feat(ble): add PolarH10BleManager for concurrent HR, RR, ECG, and ACC…
StuGotz Oct 20, 2026
358a6b1
feat(ui): add Polar H10 settings pairing card and live workout teleme…
StuGotz Oct 20, 2026
c373e65
feat(polar): add Polar H10 UI, live workout strip, and 50Hz ACC delta…
StuGotz Oct 20, 2026
aeda042
feat(polar): add migration 58, 3D torso biomechanics, auto-reconnect,…
StuGotz Oct 20, 2026
fe00387
feat(polar): add 5-minute post-workout HRR cooldown and set summary a…
StuGotz Oct 20, 2026
5078b8a
feat(polar): add 0-deg posture tare, HistoryTab integration, and tele…
StuGotz Oct 20, 2026
3d4105a
fix(polar): resolve ConcurrentModificationException, pitch sign, cool…
StuGotz Oct 20, 2026
1739093
fix(polar): run cooldown only on final completion, move Polar cards b…
StuGotz Oct 20, 2026
a768a61
fix(polar): exclude raw HR telemetry when backup toggle is off, reord…
StuGotz Oct 20, 2026
eb9ea71
fix(local): consolidate migration 58 statements and fix syntax in Mig…
StuGotz Oct 20, 2026
c8cbe67
fix(polar): ignore and drop telemetry streams upon strap removal or d…
StuGotz Oct 20, 2026
5cde73d
fix(polar): drop and suppress telemetry noise instantly when electrod…
StuGotz Oct 20, 2026
1ed0af4
fix(polar): resolve PR review findings including FK telemetry persist…
StuGotz Oct 21, 2026
135b177
fix(polar): resolve reviewer requested architectural, DI, schema pari…
StuGotz Oct 24, 2026
d2624ee
fix(polar): serialize PMD stream startup for Set 1 ECG and fix post-w…
StuGotz Oct 26, 2026
053080b
fix(polar): fix Kable initial Disconnected state race, GATT close lea…
StuGotz Oct 26, 2026
728f957
fix(polar): resolve phoneMain rebase, fix auto-reconnect & chart load…
StuGotz Oct 28, 2026
2c722a2
fix(polar): preserve lastRecordedSessionId across routine Idle transi…
StuGotz Oct 28, 2026
50873a8
fix(polar): remove committedSessionIds and apply residual PR #1326 re…
StuGotz Oct 28, 2026
afbbcba
fix(polar): address latest kilo-code PR review feedback
StuGotz Oct 28, 2026
f686067
fix(polar): resolve telemetry lifecycle and review findings
9thLevelSoftware Oct 29, 2026
2b11260
fix(polar): import IO dispatcher for native targets
9thLevelSoftware Oct 29, 2026
9dd0dc8
test: make deferred PR weight conversion deterministic
9thLevelSoftware Oct 29, 2026
a29fb71
test(polar): use a native-compatible parser test name
9thLevelSoftware Oct 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ The schema version is derived from the migration files (highest `N.sqm` + 1). Do
5. Echo achieved load (#1182): an Echo set with no measured load pushes the non-null `weightKg = 0` sentinel with `estimatedOneRepMaxKg = null`. Before this ships, deploy the portal guard: `buildExerciseProgressRows` must not write a 0 `max_weight_kg`/`estimated_1rm_kg`/`total_volume_kg` progress row for such an exercise, and session detail must render 0-weight Echo sets as "Load unavailable", not "0 kg".
6. Install a minified release APK (R8 keep rules were narrowed in #960; PR CI builds debug only) and verify on device: BLE connect and a workout, sync push/pull, backup export/import, settings persistence across restart, CSV import/export, and a Health Connect write.
7. Custom equipment (#1227): deploy the portal's `CUSTOM_EQUIPMENT` profile-preference section first (additive migration, `profilePreferenceContract.ts`, `mobile-sync-pull` and manifest emission; portal branch `enhancement/1227-custom-equipment`). Until it is deployed the portal rejects the section, so each device keeps its list local and dirty. Devices on 1.0.6 or older treat `U_` custom exercises as bodyweight if they edit them, so the release notes must tell users to update every device before editing those exercises.
8. Polar H10 (#1326): complete [the Polar device release checks](docs/qa/polar-h10-release.md) on Android and iOS with both trainers, including concurrent trainer reconnect and Polar discovery on iOS. Shared CoreBluetooth scan interference remains unverified until that device check passes. Release notes must state that Polar summaries and raw traces are local plus file backup only; portal sync does not restore them.

## Sync Architecture

Expand Down
2 changes: 1 addition & 1 deletion docs/qa/persisted-file-upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This runbook verifies PR 709's physical filename migration without treating a synthetic fixture as production evidence. The automated harness is destructive only to one explicitly selected disposable emulator and one exact package name; it never clears data during an upgrade.

The harness default `-ExpectedSchemaVersion` is 59, the current database `user_version` (highest migration file + 1). Pass that parameter only to override it.
The harness default `-ExpectedSchemaVersion` is 60, the current database `user_version` (highest migration file + 1). Pass that parameter only to override it.

## Automated v0.9.6 debug-lineage matrix

Expand Down
25 changes: 25 additions & 0 deletions docs/qa/polar-h10-release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Polar H10 release notes and device checks

## User-facing release notes

- Polar H10 adds heart rate, R-R intervals, ECG preview, torso movement and optional five-minute post-workout recovery tracking.
- Workout averages and movement maxima describe the recorded set. Recovery measurements are stored separately on its session, only after the 60-second or 300-second observation window completes. Canceling early leaves unfinished measurements unavailable.
- These measurements describe a workout and are not a clinical risk assessment.
- Polar summaries and traces stay on this device and in exported file backups. They are not synchronized to Phoenix Portal. Reinstalling and pulling from the portal does not restore Polar data; restore a file backup to recover it.
- Raw ECG storage is off by default. Enable **Save raw ECG** in Polar settings to retain traces. Capture is limited to five minutes (39,000 samples) per recording, stored traces expire after 30 days, and only the newest 36,000 segments are retained. Older traces can disappear before their sessions; summaries remain available. Retention is applied when the telemetry repository opens and when it saves telemetry.
- File backups preserve summaries regardless of the raw telemetry option. Beat samples, ECG and synchronized metric samples require **Include raw telemetry** when exporting. Backup files are not changed by local trace expiry.
- Streaming and automatic reconnection start with recording. Opening the workout screen alone does not start a scan or ECG/accelerometer stream. An explicit Disconnect remains effective until the user reconnects.

## Required device evidence before release

Local automated tests do not satisfy these checks. Record app commit, OS/device, strap firmware, trainer model and observed results for each run.

1. On both Android and iOS, pair an H10 alongside a V-Form and a Trainer+. Verify the first set, multiple sets, manual stop, automatic completion and routine completion. Check that the completion UI remains visible if the trainer is switched off while recovery continues.
2. On iOS, run a trainer reconnect scan concurrently with a remembered-H10 targeted scan and a manual H10 scan. Let each scan succeed, time out and be canceled in both orders. Confirm ending either scanner does not prevent the other device reconnecting. Kable shares a CoreBluetooth central manager, so this interaction remains an explicit release gate.
3. Take the H10 out of range during connection and PMD setup. Reconnect without restarting the app. Repeat with a failed PMD subscription; standard heart-rate notifications and connection-state changes must remain observable.
4. Leave the workout screen for routine completion during recovery. Check ECG/ACC continuity while recording owns the streams, and teardown after recovery ends. Verify Disconnect in Settings prevents automatic reconnection on subsequent screen entry or set start until explicitly reconnected.
5. Cancel recovery at 20 seconds and after 60 seconds. Confirm the first stores no HRR window and the second preserves HRR60 only. Remove the strap briefly and for more than three seconds; incomplete windows must remain absent, while previously captured rows survive. Repeat with HR notifications interrupted while PMD traffic continues.
6. Verify raw ECG default-off, opt-in, five-minute capture cap, expiry and bounded storage. Export and restore with raw telemetry both enabled and disabled, and verify historical recovery values with no strap connected.
7. On physical iOS, verify telemetry transactions with the production SQLite driver while a new set starts and an earlier set finishes saving. Reopen the app and confirm parent ownership, summaries and retained raw rows.

The release remains gated on macOS/iOS compilation, hosted CI for the published patch, and these physical-device checks.
56 changes: 56 additions & 0 deletions docs/qa/polar-h10-review-1326.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# PR 1326 review resolution

Reviewed source: `afbbcbace207b977c4f0f523b40c7bc270ac0a3c` from `StuGotz/Project-Phoenix-MP`, branch `feature/polar-h10-telemetry`.

This records local remediation of the October 29 owner review and remaining earlier feedback. It is not a hosted CI or device-test receipt.

| Finding | Resolution |
| --- | --- |
| 1. Connection timeout | Timeout reaches cleanup and an error state; external cancellation still propagates. |
| 2. PMD subscription failure | Readiness deferreds complete exceptionally; PMD setup failure does not kill the connection observer. |
| 3. Recovery contaminates set summary | Freeze the set at snapshot/manual stop; recovery writes only HRR columns. |
| 4. Early cancellation mislabels HRR | Persist only elapsed windows, using fresh HR observations. |
| 5. Main-thread database flush | Dedicated telemetry repository runs transactional saves on its IO dispatcher; callers await the queued work without blocking the UI thread. |
| 6. Queries for non-Polar sessions | Check summary columns before querying beat, ECG or metric rows. |
| 7. Interrupted recovery loses rows | Detach and queue captured rows before cancellation or the next recording; debounce contact loss. |
| 8. Screen disposal stops recovery streams | Recording/cooldown owns streams; releasing a preview cannot stop an active owner. |
| 9. History hides recovery | Stored HRR renders independently of cooldown controls; grouped history selects and labels a recovery-bearing or latest sensor set. |
| 10. Disconnected trainer yields blank completion | Completion remains visible when the trainer disconnects. |
| 11. iOS concurrent scanner interference | Explicit physical-device release gate in `polar-h10-release.md` and `CLAUDE.md`; unverified on hardware. |
| 12. Protocol and storage policy | Install command response listener before write; fail missing/rejected ACKs; require an H10 name; parse all compressed ACC blocks; raw ECG defaults off with capture, age and storage caps. |
| 13. Manual-stop summary missing | Capture the recording's Polar summary at Stop and remap its raw rows to the saved session ID. |
| 14. Backup ID conflicts | Identical rows count as skipped; differing or cross-session ID collisions report conflicts without replacing local rows. |
| 15. Misleading backup test | Name and assertions explicitly distinguish retained summaries from excluded raw rows. |
| 16. Explicit disconnect overridden | Automatic reconnection respects the disconnect flag; screen entry no longer initiates reconnection. |
| 17. Shared-state races | Lock advertisement access and recording state; capture nullable heart-rate values before use. |
| 18. Global state on unrelated cards | Live connection and cooldown state require the matching session; read-only history cannot expose unrelated live state. |
| 19. Clinical labels | Replace risk classifications with observed values and neutral wording. |

Earlier feedback also addressed: remove dead coordinator-ID fallbacks and duplicated cooldown guards; move storage ownership and row mapping into repositories/domain models; remove shipped testing stream toggles, screen-entry stream startup, stale migration-58 comments and unused imports; correct five schema-60 QA markers. Polar data remains local plus file backup only, documented in the release notes.

Additional review coverage checks delayed commits across recordings, frozen summaries, transient contact loss with the watchdog running, HR-only signal loss while PMD continues, missing-parent batches after session deletion, and raw-ECG retention boundaries.

## Validation

- Initial Android unit run: 26 tests, zero failures, errors or skips. This includes the persisted-file harness contract that failed on the PR head.
- Initial Android debug assembly completed.
- Initial shared test compilation failed on two invalid Compose assertion imports; those imports were removed.
- Initial lint failed inside its IntelliJ PSI analysis of the new Compose test while source edits were in progress. The frozen-source retry passed.
- Shared full run: 5,441 cases, 5,429 passing, three failing and nine skipped. The failures were outdated expectations: a resumed set must own a new session ID, and disconnected completion content now shares the connected-content branch. After correcting those tests, the exact three failed cases passed their targeted rerun; successful tests were not repeated. Combined result: 5,432 passed, nine skipped, no outstanding failures.
- `:androidApp:lintDebug` passed; after the two test files changed, the affected `:shared:lintAnalyzeAndroidHostTest` also passed.
- Updated `:androidApp:assembleDebug` passed. The local build uses `-Pskip.supabase.check=true` and does not establish a live sync/provider gate.
- Schema manifest validation passed: 681 columns across 60 tables. The full shared run includes schema parity, Polar manager/parser/persistence/repository tests, backup completeness, manual-stop regression and Compose history regressions.
- `git diff --check` passed before the final test-expectation-only corrections.
- Physical Android/iOS BLE, native iOS SQLite, macOS/iOS compilation and hosted CI remain external release gates.

Commands used (PowerShell quotes are required around the Gradle project property):

```powershell
.\gradlew.bat '-Pskip.supabase.check=true' :shared:testAndroidHostTest :androidApp:testDebugUnitTest :androidApp:lintDebug :androidApp:assembleDebug --continue
.\gradlew.bat '-Pskip.supabase.check=true' :shared:testAndroidHostTest :androidApp:lintDebug :androidApp:assembleDebug --continue
.\gradlew.bat '-Pskip.supabase.check=true' :shared:testAndroidHostTest --tests '*PolarH10BleManagerTest.routine intermediate sets update phase to REST while final completion triggers cooldown' --tests '*Issue687WorkoutStartGateUiTest.recovery actions remain available while disconnected' --tests '*Issue687WorkoutStartGateUiTest.recovery actions remain available while reconnecting' :shared:lintAnalyzeAndroidHostTest --continue
```

The first attempt with an unquoted property stopped during configuration before compiling or testing. Logs are in the local temporary directory: `phoenix-pr1326-validation.log`, `phoenix-pr1326-validation-final.log`, and `phoenix-pr1326-targeted-recheck.log`.

Publication details and hosted checks are recorded on the PR. This report does not establish review approval, merge or deployment.
6 changes: 3 additions & 3 deletions docs/qa/profile-schema42-fixture.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Profile schema-42 Android fixture

This recipe materializes the pre-profile database and legacy preference input used to prove the schema-42 to current-schema upgrade. The fixture source is commit `ac84d9bb8e156002833ad526bf324a8f12710da0`; do not substitute a moving branch or shortened SHA. The upgrade candidate is a debug APK from the current checkout. After that install, `PRAGMA user_version` must be 59 (highest migration file + 1).
This recipe materializes the pre-profile database and legacy preference input used to prove the schema-42 to current-schema upgrade. The fixture source is commit `ac84d9bb8e156002833ad526bf324a8f12710da0`; do not substitute a moving branch or shortened SHA. The upgrade candidate is a debug APK from the current checkout. After that install, `PRAGMA user_version` must be 60 (highest migration file + 1).

All commands below are PowerShell commands. Start at the repository root. They use the installed Android SDK (`ANDROID_HOME`, else `ANDROID_SDK_ROOT`, else `%LOCALAPPDATA%\Android\Sdk`) and must not contact Supabase. The only network use is the explicit `git fetch` of the pinned fixture commit and Gradle dependency resolution for that commit. Do not pass `--offline`.

Expand Down Expand Up @@ -171,9 +171,9 @@ foreach ($suffix in @('-wal', '-shm')) {
if ($LASTEXITCODE -eq 0) { Receive-SandboxBytes $relative "$inspectDb$suffix" -AllowEmpty }
}
$userVersion = (& $hostSqlite -readonly -batch $inspectDb 'PRAGMA user_version;').Trim()
if ($userVersion -ne '59') { throw "Expected user_version 59 after upgrade, got $userVersion" }
if ($userVersion -ne '60') { throw "Expected user_version 60 after upgrade, got $userVersion" }
& $hostSqlite -readonly -batch $inspectDb 'SELECT profile_id, legacy_migration_version, body_weight_kg, weight_unit, weight_increment, led_color_scheme_id, equipment_rack_json, workout_preferences_json, vbt_preferences_json FROM UserProfilePreferences ORDER BY profile_id;'
& $adb shell run-as $package cat shared_prefs/phoenix_preferences.xml
```

The host `sqlite3` query must report `user_version` 59, each existing profile row must have `legacy_migration_version = 1`, and the sentinel values from the tracked XML must appear in the corresponding profile preference sections.
The host `sqlite3` query must report `user_version` 60, each existing profile row must have `legacy_migration_version = 1`, and the sentinel values from the tracked XML must appear in the corresponding profile preference sections.
2 changes: 1 addition & 1 deletion scripts/qa/Invoke-PhoenixPersistedFileUpgrade.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ param(
[string] $EvidenceDirectory = (Join-Path ([IO.Path]::GetTempPath()) 'phoenix-persisted-file-upgrade'),

[Parameter()]
[int] $ExpectedSchemaVersion = 59
[int] $ExpectedSchemaVersion = 60
)

Set-StrictMode -Version Latest
Expand Down
Loading
Loading