Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .almanac/pages/machine-diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ The screen is driven by live BLE state. `BleRepository` exposes `diagnostics` as

The binary contract is broader than just four fault words. `parseDiagnosticPacket()` accepts an empty payload as a valid zero snapshot, rejects non-empty payloads shorter than `18` bytes, then decodes uptime seconds, four unsigned `16-bit` fault words, six required temperatures, two optional extra temperatures, an optional `52`-byte crash block, and an optional `32-bit` warnings field [@protocol-parser] [@protocol-models]. [[vitruvian-ble-protocol]] is the neighboring page for the broader scan, rep, monitor, and command surface around this diagnostic characteristic.

Fault decoding is intentionally category-specific. `DiagnosticFaultDecoder` always projects the packet into four display slots named `Vee`, `Other`, `Motor A`, and `Motor B`, then maps bitmasks to labels such as `TI restarted`, `Over voltage`, `Encoder`, or `Motor overtemp` instead of exposing only raw integers [@fault-decoder]. The view-model tests pin that labeling contract and verify that the export text includes both the human label and the raw hex code [@diagnostics-tests].
Fault decoding is intentionally category-specific. `DiagnosticFaultDecoder` always projects the packet into four display slots named `Controller`, `Other`, `Motor A`, and `Motor B`, then maps bitmasks to labels such as `Controller restarted`, `Overvoltage`, `Encoder fault`, or `Motor over-temperature` instead of exposing only raw integers [@fault-decoder]. The view-model tests pin that labeling contract and verify that the export text includes both the human label and the raw hex code [@diagnostics-tests].

The export contract is deliberately narrow. `buildDiagnosticsExportText()` prepends app version, export timestamp, `REDACTED_DIAGNOSTICS` classification, and a privacy line that excludes workout history, profiles, auth or session tokens, Supabase config, and keystore data before listing the current packet contents [@diagnostics-vm]. The copy button stays disabled until a packet exists, so the feature exports the latest machine snapshot rather than an empty template [@diagnostics-screen].

Expand Down
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ body:
id: app-version
attributes:
label: App Version
description: What version of Vitruvian Phoenix are you using?
description: What version of Project Phoenix are you using?
placeholder: "e.g., 1.0.0"
validations:
required: true
Expand Down
2 changes: 1 addition & 1 deletion .phoenix-review/BUG-FIX-PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@
- F173 | impact=250 | Domain | medium / failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/domain/voice/SafeWordListener.kt | lines=26, 38-44 | The common listener contract exposes `startListening()`, `isListening`, and `detectedWord`, but no error/status channel for unavailable recognition, denied microphone/speech permission, unsupported on-device recognition, or start failures. The platform implementations can fail by logging and leaving `isListening` false, which leaves callers unable to distinguish “not yet started”, “already stopped”, and “emergency voice stop is unavailable”. For a safety-oriented emergency stop path, silent non-operation can make the workout UI appear configured while no voice stop will ever fire. | fix: Add a shared status/error flow or sealed state such as `Idle`, `Listening`, `PermissionDenied`, `RecognitionUnavailable`, and `StartFailed`, and have calibration/workout UI surface failures instead of relying only on logs and `isListening`.
- F174 | impact=250 | Domain | medium / failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/domain/voice/SafeWordListener.kt | lines=41-44 | The API documents that emitted detections occur when results “match the safe word”, but it does not define or enforce what a valid safe word is. The current platform implementations perform whole-token matching after splitting transcripts on whitespace, while settings/calibration paths only require a non-blank string. A user can configure a multi-word phrase or punctuation-bearing value that appears valid in common code but can never match the actual token-based listener. | fix: Define the safe-word normalization/validation contract in common code. Either restrict configured safe words to one normalized token and reject/trim punctuation at save/calibration time, or update platform matching to normalize transcripts and support the same phrase rules accepted by the UI.
- F304 | impact=250 | Utilities | medium / bug | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/DataBackupManager.kt | lines=1355-1380 | The streaming personal-record importer drops `PersonalRecordBackup.cableCount` by passing `cable_count = null` to `queries.upsertPR`. The non-streaming importer preserves this field at lines 631-643, so large backups imported through the streaming path lose cable-count metadata while small backups do not. | fix: Pass `pr.cableCount?.toLong()` in the streaming path to match `importFromJson()`.
- F306 | impact=250 | Utilities | medium / bug | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/BlePacketFactory.kt | lines=75-86 | `createStopCommand()` is documented as the primary STOP command but emits legacy opcode `0x05`, while `BleConstants.Commands.STOP_COMMAND` and `createOfficialStopPacket()` use official stop opcode `0x50`. This creates an attractive but unsafe API footgun: callers choosing `createStopCommand()` by name/comment may send a different stop semantics than the official stop/clear-fault packet. | fix: Rename/deprecate the legacy helper (for example `createLegacyStopCommand()`), make the primary helper delegate to `createOfficialStopPacket()`, and keep tests explicit about the legacy opcode only where it is intentionally required.
- F306 | impact=250 | Utilities | medium / bug | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/BlePacketFactory.kt | lines=75-86 | `createStopCommand()` is documented as the primary STOP command but emits legacy opcode `0x05`, while `BleConstants.Commands.STOP_COMMAND` and `createSoftStopPacket()` use 0x50 soft-stop opcode `0x50`. This creates an attractive but unsafe API footgun: callers choosing `createStopCommand()` by name/comment may send a different stop semantics than the 0x50 soft-stop/clear-fault packet. | fix: Rename/deprecate the legacy helper (for example `createLegacyStopCommand()`), make the primary helper delegate to `createSoftStopPacket()`, and keep tests explicit about the legacy opcode only where it is intentionally required.
- F311 | impact=250 | Utilities | medium / bug | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/HardwareDetection.kt | lines=8-13, 20-29 | The file-level documentation says name-prefix hardware detection was flawed and should be avoided, but `detectModel()` still infers `VFormTrainer`/`TrainerPlus` from `Vee_` and `VIT` prefixes. That stale detection result is used when reporting `ConnectionState.Connected`, so UI or downstream logic can still receive a model classification the comments say is unreliable. | fix: Either remove model inference and return `Unknown` until firmware-backed detection exists, or update the documentation and tests to explain why these prefixes are now reliable. Prefer firmware/version-characteristic detection before exposing a concrete hardware model.
- F313 | impact=250 | Utilities | medium / bug | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/KmpUtils.kt | lines=232-236 | `formatFloat()` builds the decimal part from `rounded - intPart`, but `toLong()` truncates negative values toward zero. Negative values with decimal places therefore format with a negative fractional component, e.g. the current algorithm formats `-1.23` with two decimals as `-1.-23` and `-0.25` as `0.-25`. Any UI using this helper for signed deltas, percentages, velocities, or corrections can display malformed numbers. | fix: Format the absolute fractional component separately from the sign, or use a locale-stable multiplatform formatter that rounds the whole scaled integer first and then reconstructs sign, integer, and padded fractional digits from absolute values. Add tests for negative values such as `-1.23`, `-5.5`, and `-0.25`.
- F314 | impact=250 | Utilities | medium / bug | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/KmpUtils.kt | lines=245, 283 | `formatDouble()` downcasts every `Double` to `Float` before formatting. This loses precision for normal `Double` values and turns large finite doubles outside the `Float` range into infinities, which then hit the non-finite guard and display as zero. The `Double.format()` extension exposes this behavior broadly, so any future Double-valued analytics/statistics display can silently show rounded or completely wrong values. | fix: Implement `formatDouble()` using Double arithmetic end-to-end and a Double finite check rather than delegating through Float. Add regression tests with a high-precision value and a finite value greater than `Float.MAX_VALUE` to prevent silent zeroing.
Expand Down
2 changes: 1 addition & 1 deletion .phoenix-review/CONSOLIDATED-REPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@
- F303 | module=Utilities | category=failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/DataBackupManager.kt | lines=1138-1215 | description: Metric batching counts only metrics that are actually inserted (`batchCount++` is inside `metric.sessionId in importedSessionIds`). If a large backup contains mostly metrics for skipped/pre-existing sessions, the second batching loop can consume the entire remaining `metricSamples` array in one transaction because `batchCount` never reaches `IMPORT_BATCH_SIZE`. This defeats the OOM/lock-avoidance goal of streaming import for exactly the large-file path. | fix: Limit each transaction by rows processed/seen, not rows inserted, or maintain separate `rowsInBatch` and `insertedInBatch` counters.
- F304 | module=Utilities | category=bug | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/DataBackupManager.kt | lines=1355-1380 | description: The streaming personal-record importer drops `PersonalRecordBackup.cableCount` by passing `cable_count = null` to `queries.upsertPR`. The non-streaming importer preserves this field at lines 631-643, so large backups imported through the streaming path lose cable-count metadata while small backups do not. | fix: Pass `pr.cableCount?.toLong()` in the streaming path to match `importFromJson()`.
- F305 | module=Utilities | category=failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/DataBackupManager.kt | lines=976-980, 1025-1737 | description: The streaming importer is order-dependent even though JSON object member order is not semantically significant. Child arrays such as `metricSamples`, `routineExercises`, `cycleDays`, `plannedSets`, and `completedSets` are imported only if their parent IDs have already been seen in earlier fields. A valid backup with fields reordered can silently skip children or log warnings while returning success. | fix: Make streaming import robust to object order by doing multiple passes over a seekable source, buffering small parent/child ID sets and deferred child rows, or enforcing/exporting a documented field order and rejecting out-of-order backups instead of silently skipping data.
- F306 | module=Utilities | category=bug | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/BlePacketFactory.kt | lines=75-86 | description: `createStopCommand()` is documented as the primary STOP command but emits legacy opcode `0x05`, while `BleConstants.Commands.STOP_COMMAND` and `createOfficialStopPacket()` use official stop opcode `0x50`. This creates an attractive but unsafe API footgun: callers choosing `createStopCommand()` by name/comment may send a different stop semantics than the official stop/clear-fault packet. | fix: Rename/deprecate the legacy helper (for example `createLegacyStopCommand()`), make the primary helper delegate to `createOfficialStopPacket()`, and keep tests explicit about the legacy opcode only where it is intentionally required.
- F306 | module=Utilities | category=bug | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/BlePacketFactory.kt | lines=75-86 | description: `createStopCommand()` is documented as the primary STOP command but emits legacy opcode `0x05`, while `BleConstants.Commands.STOP_COMMAND` and `createSoftStopPacket()` use 0x50 soft-stop opcode `0x50`. This creates an attractive but unsafe API footgun: callers choosing `createStopCommand()` by name/comment may send a different stop semantics than the 0x50 soft-stop/clear-fault packet. | fix: Rename/deprecate the legacy helper (for example `createLegacyStopCommand()`), make the primary helper delegate to `createSoftStopPacket()`, and keep tests explicit about the legacy opcode only where it is intentionally required.
- F307 | module=Utilities | category=failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/ConnectivityChecker.kt | lines=7-12 | description: The common contract only exposes a synchronous `isOnline()` method and has no lifecycle/dispose hook or observable connectivity-change signal. Platform implementations that allocate long-lived resources or background monitors cannot be cancelled through the common API, and common sync code cannot subscribe to a reconnect event after setting a waiting-for-connectivity state. | fix: Add lifecycle and state-change semantics to the expect contract, such as `close()`/`dispose()` plus a `StateFlow<Boolean>` or callback registration, and make DI scopes own the checker lifecycle explicitly.
- F308 | module=Utilities | category=bug | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/Constants.kt | lines=139-142 | description: `ProtocolConstants` declares packet sizes that contradict the actual packet builders and tests. `ECHO_PACKET_SIZE` is `29`, but `BlePacketFactory.createEchoControl()` builds a 32-byte frame and tests assert 32. `ACTIVATION_PACKET_SIZE` is `97`, but `BleConstants.ActivationPacket.SIZE`, `BlePacketFactory.createProgramParams()`, model documentation, and tests all use 96 bytes. Any code using these constants for allocation, validation, or MTU checks would reject valid packets or allocate the wrong size. | fix: Define packet sizes from a single source of truth (`BleConstants.ActivationPacket.SIZE` and the factory Echo size), update Echo to 32 and activation to 96, and add tests covering `ProtocolConstants` parity with factory output.
- F309 | module=Utilities | category=failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/DeviceInfo.kt | lines=71-73 | description: The common API promises device info as a JSON `String`, but the contract does not require escaping or a structured serializer-backed result. Platform values such as model names, device names, build strings, or bundle values can contain quotes, backslashes, or control characters; manual string-building actual implementations can then return invalid JSON or malformed metadata. | fix: Replace `toJson(): String` with a serializable data model or require all actual implementations to use `kotlinx.serialization`/a shared escaping helper. Add tests with quotes, backslashes, and newlines in representative fields.
Expand Down
4 changes: 2 additions & 2 deletions .phoenix-review/utilities-part-2.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ Findings: 11 total
- Category: bug
- Severity: medium
- Line numbers: 75-86
- Description: `createStopCommand()` is documented as the primary STOP command but emits legacy opcode `0x05`, while `BleConstants.Commands.STOP_COMMAND` and `createOfficialStopPacket()` use official stop opcode `0x50`. This creates an attractive but unsafe API footgun: callers choosing `createStopCommand()` by name/comment may send a different stop semantics than the official stop/clear-fault packet.
- Suggested fix direction: Rename/deprecate the legacy helper (for example `createLegacyStopCommand()`), make the primary helper delegate to `createOfficialStopPacket()`, and keep tests explicit about the legacy opcode only where it is intentionally required.
- Description: `createStopCommand()` is documented as the primary STOP command but emits legacy opcode `0x05`, while `BleConstants.Commands.STOP_COMMAND` and `createSoftStopPacket()` use 0x50 soft-stop opcode `0x50`. This creates an attractive but unsafe API footgun: callers choosing `createStopCommand()` by name/comment may send a different stop semantics than the 0x50 soft-stop/clear-fault packet.
- Suggested fix direction: Rename/deprecate the legacy helper (for example `createLegacyStopCommand()`), make the primary helper delegate to `createSoftStopPacket()`, and keep tests explicit about the legacy opcode only where it is intentionally required.

#### Finding 3
- Category: bug
Expand Down
10 changes: 5 additions & 5 deletions ANDROID_INSTALL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Android Installation Guide

This guide explains how to install Vitruvian Phoenix on your Android device.
This guide explains how to install Project Phoenix on your Android device.

## Prerequisites

Expand Down Expand Up @@ -47,7 +47,7 @@ Android requires permission to install apps from outside the Play Store.
1. Open the downloaded APK file
2. Tap **Install**
3. Wait for installation to complete
4. Tap **Open** or find **Vitruvian Phoenix** in your app drawer
4. Tap **Open** or find **Project Phoenix** in your app drawer

---

Expand All @@ -64,7 +64,7 @@ When you first launch the app, you'll be asked to grant permissions:
- This is an Android requirement - the app does not track your location
- Tap **Allow** when prompted

**Note:** If you deny permissions, the app cannot connect to your trainer. You can always grant permissions later in Settings > Apps > Vitruvian Phoenix > Permissions.
**Note:** If you deny permissions, the app cannot connect to your trainer. You can always grant permissions later in Settings > Apps > Project Phoenix > Permissions.

---

Expand Down Expand Up @@ -95,12 +95,12 @@ When a new version is released:
### App Crashes on Launch

- Make sure your device is running Android 8.0 or later
- Try clearing app data: Settings > Apps > Vitruvian Phoenix > Storage > Clear data
- Try clearing app data: Settings > Apps > Project Phoenix > Storage > Clear data
- Report the issue on GitHub with your device model and Android version

### Bluetooth Permission Denied

1. Go to **Settings > Apps > Vitruvian Phoenix > Permissions**
1. Go to **Settings > Apps > Project Phoenix > Permissions**
2. Enable **Nearby devices** (Android 12+) or **Location** (Android 11 and below)
3. Restart the app

Expand Down
2 changes: 1 addition & 1 deletion androidApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ android {
targetSdk = 37
// Fail fast if CI injects an invalid version code instead of silently shipping a default.
versionCode = injectedVersionCode ?: 5
versionName = "0.9.6"
versionName = "1.0.0"

// Supabase config injected from local.properties
buildConfigField("String", "SUPABASE_URL", "\"$supabaseUrl\"")
Expand Down
2 changes: 1 addition & 1 deletion androidApp/src/main/res/xml/backup_rules.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- API 23-30 backup exclusion rules (android:fullBackupContent) -->
<!-- Excludes Vitruvian database and sensitive preferences from Android auto-backup -->
<!-- Excludes the app database and sensitive preferences from Android auto-backup -->
<full-backup-content>
<exclude domain="database" path="vitruvian.db" />
<exclude domain="database" path="vitruvian.db-journal" />
Expand Down
2 changes: 1 addition & 1 deletion androidApp/src/main/res/xml/data_extraction_rules.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- API 31+ data extraction rules (android:dataExtractionRules) -->
<!-- Excludes Vitruvian database and sensitive preferences from both cloud backup and device transfer -->
<!-- Excludes the app database and sensitive preferences from both cloud backup and device transfer -->
<data-extraction-rules>
<cloud-backup>
<exclude domain="database" path="vitruvian.db" />
Expand Down
Loading
Loading