diff --git a/.agents/skills/update-phoenix-version/SKILL.md b/.agents/skills/update-phoenix-version/SKILL.md index 69df1d6b9..1a89c42be 100644 --- a/.agents/skills/update-phoenix-version/SKILL.md +++ b/.agents/skills/update-phoenix-version/SKILL.md @@ -15,13 +15,13 @@ The four app-version values to keep aligned are: - `androidApp/build.gradle.kts`: Android `versionName`. - `shared/src/commonMain/kotlin/com/devil/phoenixproject/util/Constants.kt`: `Constants.APP_VERSION`, used by Android `DeviceInfo`, Settings display, and backup metadata. -- `iosApp/VitruvianPhoenix/VitruvianPhoenix.xcodeproj/project.pbxproj`: Debug `MARKETING_VERSION`. -- `iosApp/VitruvianPhoenix/VitruvianPhoenix.xcodeproj/project.pbxproj`: Release `MARKETING_VERSION`. +- `iosApp/PhoenixApp/PhoenixApp.xcodeproj/project.pbxproj`: Debug `MARKETING_VERSION`. +- `iosApp/PhoenixApp/PhoenixApp.xcodeproj/project.pbxproj`: Release `MARKETING_VERSION`. The helper can also update build-number fields near those values: - `androidApp/build.gradle.kts`: default `versionCode = injectedVersionCode ?: ...` when `--android-code` is supplied. -- `iosApp/VitruvianPhoenix/VitruvianPhoenix.xcodeproj/project.pbxproj`: both `CURRENT_PROJECT_VERSION` entries when `--ios-build` is supplied. +- `iosApp/PhoenixApp/PhoenixApp.xcodeproj/project.pbxproj`: both `CURRENT_PROJECT_VERSION` entries when `--ios-build` is supplied. Do not edit `shared/src/commonMain/composeResources/*/strings.xml` for a version bump. `settings_version` is only the localized label template. Do not edit `androidApp/release/output-metadata.json`; it is generated release output. diff --git a/.agents/skills/update-phoenix-version/scripts/update_version.py b/.agents/skills/update-phoenix-version/scripts/update_version.py index e609109da..b54e90b94 100644 --- a/.agents/skills/update-phoenix-version/scripts/update_version.py +++ b/.agents/skills/update-phoenix-version/scripts/update_version.py @@ -122,7 +122,7 @@ def make_targets(args: argparse.Namespace) -> list[Target]: ), Target( label="iOS MARKETING_VERSION", - relative_path=Path("iosApp/VitruvianPhoenix/VitruvianPhoenix.xcodeproj/project.pbxproj"), + relative_path=Path("iosApp/PhoenixApp/PhoenixApp.xcodeproj/project.pbxproj"), pattern=re.compile(r"^(\s*MARKETING_VERSION\s*=\s*)([^;]+)(;.*)$", re.MULTILINE), expected_matches=2, replacement_value=version, @@ -147,7 +147,7 @@ def make_targets(args: argparse.Namespace) -> list[Target]: targets.append( Target( label="iOS CURRENT_PROJECT_VERSION", - relative_path=Path("iosApp/VitruvianPhoenix/VitruvianPhoenix.xcodeproj/project.pbxproj"), + relative_path=Path("iosApp/PhoenixApp/PhoenixApp.xcodeproj/project.pbxproj"), pattern=re.compile(r"^(\s*CURRENT_PROJECT_VERSION\s*=\s*)([0-9]+)(;.*)$", re.MULTILINE), expected_matches=2, replacement_value=args.ios_build, diff --git a/.almanac/pages/data-backup-and-repair.md b/.almanac/pages/data-backup-and-repair.md index 982345e12..bb704aaa2 100644 --- a/.almanac/pages/data-backup-and-repair.md +++ b/.almanac/pages/data-backup-and-repair.md @@ -45,7 +45,7 @@ sources: note: Defines the persisted default-versus-custom destination model and iOS bookmark storage. - id: app-startup type: file - path: androidApp/src/main/kotlin/com/devil/phoenixproject/VitruvianApp.kt + path: androidApp/src/main/kotlin/com/devil/phoenixproject/PhoenixApp.kt note: Shows that startup migration and repair runs immediately after Koin initialization on Android. - id: backup-routing-test type: file @@ -82,7 +82,7 @@ Pruning behavior is also platform-specific. Android queries Downloads entries an ## Startup repair -Startup repair is part of normal app boot. Android calls `migrationManager.checkAndRunMigrations()` during `VitruvianApp.onCreate()`, and the migration manager then refreshes profiles, strips fabricated `legacy_session_` routine session IDs, normalizes legacy workout-mode names, backfills bad routine names on old workout rows, repairs PRs from workout history, audits profile-scoped data, and checks for orphaned records [@app-startup] [@migration-manager] [@migration-tests]. +Startup repair is part of normal app boot. Android calls `migrationManager.checkAndRunMigrations()` during `PhoenixApp.onCreate()`, and the migration manager then refreshes profiles, strips fabricated `legacy_session_` routine session IDs, normalizes legacy workout-mode names, backfills bad routine names on old workout rows, repairs PRs from workout history, audits profile-scoped data, and checks for orphaned records [@app-startup] [@migration-manager] [@migration-tests]. Profile-scope repair can become interactive when old default-profile rows and current active-profile rows both exist. `ProfileScopeRepairState.NeedsChoice` carries both row counts plus the active profile identity so the app can either move legacy `default`-scoped data into the active profile or switch back to the default profile without moving rows [@migration-state] [@migration-manager]. Read [[profiles]] with this page when the symptom is "data disappeared after I changed profiles" rather than a failed restore or broken file export. diff --git a/.almanac/pages/data.md b/.almanac/pages/data.md index 661e149fa..9769f3e81 100644 --- a/.almanac/pages/data.md +++ b/.almanac/pages/data.md @@ -9,7 +9,7 @@ sources: note: Defines the SQLDelight database version and schema-manifest validation task. - id: schema-file type: file - path: shared/src/commonMain/sqldelight/com/devil/phoenixproject/database/VitruvianDatabase.sq + path: shared/src/commonMain/sqldelight/com/devil/phoenixproject/database/PhoenixDatabase.sq note: Defines the broad shared schema, including profile-scoped workout, routine, sync, and analytics tables. - id: migration-manager type: file @@ -48,7 +48,7 @@ Phoenix has one shared persistence cluster, but future tasks usually hit it thro Read this hub when the symptom is about where state lives, why rows moved, or why one feature is seeing data produced by another. The data cluster spans [[local-data-model]] for schema and repair mechanics, [[profiles]] for active-profile visibility and deletion reassignment, [[data-backup-and-repair]] for backup, restore, auto-backup, and import-time adoption rules, [[routines-and-training-cycles]] for persisted workout-programming state, [[strength-assessment-and-insights]] for stored 1RM and Smart Insights state, [[gamification]] for badges, streaks, and RPG-summary state, [[equipment-rack]] for settings-backed accessory inventory plus per-session rack snapshots, and [[csv-workout-import-export]] or [[external-provider-sync]] for external data that eventually lands in local tables [@schema-file] [@profile-repo] [@backup-manager] [@csv-importer] [@sync-manager] [@gamification-page]. -The key boundary is that Phoenix data is shared even when features are not. `VitruvianDatabase.sq` keeps workout, routine, assessment, badge, streak, sync, and external-activity entities in one schema; `MigrationManager` then runs startup repair across that shared space; and `DataBackupManager` exports and imports nearly all of it as one backup surface [@schema-file] [@migration-manager] [@backup-manager]. +The key boundary is that Phoenix data is shared even when features are not. `PhoenixDatabase.sq` keeps workout, routine, assessment, badge, streak, sync, and external-activity entities in one schema; `MigrationManager` then runs startup repair across that shared space; and `DataBackupManager` exports and imports nearly all of it as one backup surface [@schema-file] [@migration-manager] [@backup-manager]. ## Default read order diff --git a/.almanac/pages/external-provider-sync.md b/.almanac/pages/external-provider-sync.md index 436d816c4..5a18669fb 100644 --- a/.almanac/pages/external-provider-sync.md +++ b/.almanac/pages/external-provider-sync.md @@ -37,7 +37,7 @@ sources: note: Shows activity storage, provider status storage, and provider-scoped deletes. - id: db-schema type: file - path: shared/src/commonMain/sqldelight/com/devil/phoenixproject/database/VitruvianDatabase.sq + path: shared/src/commonMain/sqldelight/com/devil/phoenixproject/database/PhoenixDatabase.sq note: Defines integration status and cursor tables and the provider-scoped local schema surface. status: active verified: 2026-06-25 diff --git a/.almanac/pages/frontend.md b/.almanac/pages/frontend.md index e63776cbe..11f9ca18b 100644 --- a/.almanac/pages/frontend.md +++ b/.almanac/pages/frontend.md @@ -29,7 +29,7 @@ sources: note: Defines the routine-editor and training-cycle programming boundary that can surface through shared Compose screens without being part of the live session engine. - id: ios-content type: file - path: iosApp/VitruvianPhoenix/VitruvianPhoenix/ContentView.swift + path: iosApp/PhoenixApp/PhoenixApp/ContentView.swift note: Shows that iOS hosts the shared Compose controller rather than a parallel SwiftUI screen tree. - id: android-host type: file diff --git a/.almanac/pages/getting-started.md b/.almanac/pages/getting-started.md index 6eff16375..143e615c4 100644 --- a/.almanac/pages/getting-started.md +++ b/.almanac/pages/getting-started.md @@ -170,8 +170,8 @@ Keep [[app-architecture]] nearby when the remaining question is which manager, r - New shared-UI feature where the owning feature boundary is still unclear: [[frontend]] -> [[app-architecture]] -> [[workouts]] or [[integrations]] [@frontend-page] [@architecture-page] [@workouts-page] [@integrations-page] - Shared screen or route bug where the owning feature is still unclear: [[frontend]] -> [[app-architecture]] -> [[workouts]] or [[integrations]] - Settings-tab issue that could still be auth, integration, backup, or workout-preference state: [[settings-surface]] -> [[auth]] or [[integrations]] or [[data-backup-and-repair]] or [[workout-safety-and-feedback]] [@settings-page] -- BLE bug or machine-behavior mismatch: [[project-phoenix]] -> [[workouts]] -> [[vitruvian-ble-protocol]] -- Diagnostics fault codes or crash snapshots: [[workouts]] -> [[machine-diagnostics]] -> [[vitruvian-ble-protocol]] [@diagnostics-page] +- BLE bug or machine-behavior mismatch: [[project-phoenix]] -> [[workouts]] -> [[phoenix-ble-protocol]] +- Diagnostics fault codes or crash snapshots: [[workouts]] -> [[machine-diagnostics]] -> [[phoenix-ble-protocol]] [@diagnostics-page] - Voice stop or cue playback mismatch: [[workouts]] -> [[workout-safety-and-feedback]] -> [[platform-hosts]] [@workout-safety-page] [@hosts-page] - Routine editor or training-cycle bug before live execution starts: [[workouts]] -> [[routines-and-training-cycles]] -> [[profiles]] or [[strength-assessment-and-insights]] [@routines-page] [@profiles-page] [@assessment-page] - 1RM assessment or Smart Insights issue: [[workouts]] -> [[strength-assessment-and-insights]] -> [[local-data-model]] [@assessment-page] diff --git a/.almanac/pages/local-data-model.md b/.almanac/pages/local-data-model.md index 2d81115da..ae23ea495 100644 --- a/.almanac/pages/local-data-model.md +++ b/.almanac/pages/local-data-model.md @@ -9,7 +9,7 @@ sources: note: Defines SQLDelight schema versioning and the schema manifest validation task. - id: schema-file type: file - path: shared/src/commonMain/sqldelight/com/devil/phoenixproject/database/VitruvianDatabase.sq + path: shared/src/commonMain/sqldelight/com/devil/phoenixproject/database/PhoenixDatabase.sq note: Defines the current database schema and many migration-added columns. - id: migration-manager type: file @@ -38,7 +38,7 @@ The shared database uses SQLDelight, but numbered migrations are not the whole p ## Persistence contract -The schema itself is broad and profile-aware. `VitruvianDatabase.sq` persists workout sessions, metric samples, PRs, routines, supersets, routine groups, training cycles, completed sets, progressions, gamification state, connection logs, diagnostics snapshots, sync metadata, and external integration entities, with many tables carrying `profile_id`, `updatedAt`, `serverId`, and `deletedAt` fields [@schema-file]. Read [[profiles]] alongside this page when the bug is really about active-profile filtering, delete-time reassignment, or why the same local database can show different slices of data after a profile switch. +The schema itself is broad and profile-aware. `PhoenixDatabase.sq` persists workout sessions, metric samples, PRs, routines, supersets, routine groups, training cycles, completed sets, progressions, gamification state, connection logs, diagnostics snapshots, sync metadata, and external integration entities, with many tables carrying `profile_id`, `updatedAt`, `serverId`, and `deletedAt` fields [@schema-file]. Read [[profiles]] alongside this page when the bug is really about active-profile filtering, delete-time reassignment, or why the same local database can show different slices of data after a profile switch. ## Repair layers @@ -52,7 +52,7 @@ This makes database safety here more about idempotent repair than about trusting Backup, restore, and startup-repair behavior are first-class parts of this persistence layer, but they now have their own retrieval page. Read [[data-backup-and-repair]] when the task is about streamed export or import, auto-backup timing, profile-scope repair, or other user-visible data recovery behavior [@backup-manager] [@migration-manager]. -`DiagnosticsHistory` is currently reserved schema, not an active feature path. `VitruvianDatabase.sq` and `SchemaManifest.kt` still define the table plus recent or fault-only queries, but the live diagnostics flow in `KableBleRepository` only updates the in-memory `BleRepository.diagnostics` state and connection-log stream, and `DiagnosticsViewModel` renders directly from that live state instead of reading SQLDelight history rows [@schema-file] [@kable-repo] [@diagnostics-vm]. Read [[machine-diagnostics]] with this in mind when a future task proposes persisting diagnostic snapshots, because the schema surface already exists but the current product path is live-only. +`DiagnosticsHistory` is currently reserved schema, not an active feature path. `PhoenixDatabase.sq` and `SchemaManifest.kt` still define the table plus recent or fault-only queries, but the live diagnostics flow in `KableBleRepository` only updates the in-memory `BleRepository.diagnostics` state and connection-log stream, and `DiagnosticsViewModel` renders directly from that live state instead of reading SQLDelight history rows [@schema-file] [@kable-repo] [@diagnostics-vm]. Read [[machine-diagnostics]] with this in mind when a future task proposes persisting diagnostic snapshots, because the schema surface already exists but the current product path is live-only. ## Reading boundary diff --git a/.almanac/pages/machine-diagnostics.md b/.almanac/pages/machine-diagnostics.md index 31d8df6f3..a984493b8 100644 --- a/.almanac/pages/machine-diagnostics.md +++ b/.almanac/pages/machine-diagnostics.md @@ -41,7 +41,7 @@ sources: note: Defines the shared diagnostic packet and crash payload models. - id: schema-file type: file - path: shared/src/commonMain/sqldelight/com/devil/phoenixproject/database/VitruvianDatabase.sq + path: shared/src/commonMain/sqldelight/com/devil/phoenixproject/database/PhoenixDatabase.sq note: Defines the reserved DiagnosticsHistory table and diagnostic snapshot queries in the local schema. - id: diagnostics-tests type: file @@ -54,7 +54,7 @@ Machine diagnostics is a shared troubleshooting workflow for a connected trainer The screen is driven by live BLE state. `BleRepository` exposes `diagnostics` as a `StateFlow`, `DiagnosticsViewModel` combines that stream with connection state, and `DiagnosticsScreen` renders one of three states: disconnected with no snapshot, connected but waiting for the characteristic to answer, or a populated packet with sections for uptime, faults, temperatures, crash data, and warnings [@ble-repo] [@diagnostics-vm] [@diagnostics-screen]. -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. +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]. [[phoenix-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 `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]. @@ -62,6 +62,6 @@ The export contract is deliberately narrow. `buildDiagnosticsExportText()` prepe Live publication and logging happen below the UI. `KableBleRepository.publishDiagnostics()` stamps packets with `receivedAtMillis` when needed, publishes them to the shared diagnostics flow, and emits a diagnostic log entry only when the fault-word set changes so repeated identical snapshots do not spam the log stream [@kable-repo]. -The current workflow is live-only even though the schema already reserves a history table. `VitruvianDatabase.sq` still defines `DiagnosticsHistory` plus recent and fault-only queries, but the present code path does not write packets into that table or read it back into `DiagnosticsViewModel`; the user-facing diagnostics screen is driven by the current BLE packet and the export text derived from it [@schema-file] [@kable-repo] [@diagnostics-vm]. +The current workflow is live-only even though the schema already reserves a history table. `PhoenixDatabase.sq` still defines `DiagnosticsHistory` plus recent and fault-only queries, but the present code path does not write packets into that table or read it back into `DiagnosticsViewModel`; the user-facing diagnostics screen is driven by the current BLE packet and the export text derived from it [@schema-file] [@kable-repo] [@diagnostics-vm]. -Read [[workouts]] first when the bug source is still unclear, [[vitruvian-ble-protocol]] when the question is whether the packet itself is being parsed or delivered correctly, [[local-data-model]] when the task is about turning the reserved diagnostics schema into a real persistence feature, and [[platform-hosts]] when Android and iOS disagree about the conditions under which a connected machine reaches this screen. +Read [[workouts]] first when the bug source is still unclear, [[phoenix-ble-protocol]] when the question is whether the packet itself is being parsed or delivered correctly, [[local-data-model]] when the task is about turning the reserved diagnostics schema into a real persistence feature, and [[platform-hosts]] when Android and iOS disagree about the conditions under which a connected machine reaches this screen. diff --git a/.almanac/pages/vitruvian-ble-protocol.md b/.almanac/pages/phoenix-ble-protocol.md similarity index 84% rename from .almanac/pages/vitruvian-ble-protocol.md rename to .almanac/pages/phoenix-ble-protocol.md index c655a7bca..18ffcf342 100644 --- a/.almanac/pages/vitruvian-ble-protocol.md +++ b/.almanac/pages/phoenix-ble-protocol.md @@ -1,5 +1,5 @@ --- -title: Vitruvian BLE Protocol +title: Phoenix BLE Protocol summary: BLE control is centered on a Kable-based connection manager, parser utilities that normalize multiple firmware packet formats, and shared command semantics built around per-cable loads. topics: [systems, ble, stack] sources: @@ -24,7 +24,7 @@ verified: 2026-06-25 --- `KableBleConnectionManager` owns the live `Peripheral` reference exclusively. The class comment says it was extracted from `KableBleRepository` so connection lifecycle code, notification subscriptions, readiness checks, and command sending all live in one place while repository-facing flows remain in the façade layer [@ble-manager]. [[workouts]] is the right cluster hub when the symptom might still be in routine flow, persistence, or platform behavior rather than in BLE itself. -Scan filtering is conservative and hardware-specific. The manager accepts advertisement names starting with `Vee_`, `VIT`, or `Vitruvian`, which matches the supported hardware naming patterns documented in the README and keeps the scan UI from filling with unrelated devices [@ble-manager]. +Scan filtering is conservative and hardware-specific. The manager accepts advertisement names starting with `Vee_`, `VIT`, or `Phoenix`, which matches the supported hardware naming patterns documented in the README and keeps the scan UI from filling with unrelated devices [@ble-manager]. The parser layer treats byte handling as a correctness boundary. `ProtocolParser.kt` calls out signed Kotlin bytes as a recurring hazard and masks every byte with `and 0xFF` before assembling integers, so changes to parser code need the same discipline to avoid sign-extension bugs [@protocol-parser]. @@ -38,4 +38,4 @@ Command semantics in shared code still distinguish mode families. `ProtocolConst The common BLE tests intentionally cover only what is stable without real hardware. `KableBleConnectionManagerTest` verifies opcode routing, disconnect cleanup, and empty-packet handling, while the test header explicitly says connection, scanning, and auto-reconnect behavior still need manual BLE validation [@ble-tests]. -Read [[project-phoenix]] before changing scan filters or packet compatibility rules because those branches preserve support for older Vitruvian hardware and firmware instead of optimizing for a narrower current-device contract. Read [[machine-diagnostics]] when the BLE symptom is specifically about fault decoding, diagnostic snapshots, or the troubleshooting export built on top of this parser. Read [[platform-hosts]] when Android and iOS disagree about BLE permissions, reconnects, or background workout continuity around the same shared protocol code. +Read [[project-phoenix]] before changing scan filters or packet compatibility rules because those branches preserve support for older Phoenix hardware and firmware instead of optimizing for a narrower current-device contract. Read [[machine-diagnostics]] when the BLE symptom is specifically about fault decoding, diagnostic snapshots, or the troubleshooting export built on top of this parser. Read [[platform-hosts]] when Android and iOS disagree about BLE permissions, reconnects, or background workout continuity around the same shared protocol code. diff --git a/.almanac/pages/platform-hosts.md b/.almanac/pages/platform-hosts.md index 9936855eb..f0a5ed4fb 100644 --- a/.almanac/pages/platform-hosts.md +++ b/.almanac/pages/platform-hosts.md @@ -13,7 +13,7 @@ sources: note: Defines Android secure storage, BLE, health, backup, and foreground service bindings. - id: ios-app type: file - path: iosApp/VitruvianPhoenix/VitruvianPhoenix/VitruvianPhoenixApp.swift + path: iosApp/PhoenixApp/PhoenixApp/PhoenixApp.swift note: Shows iOS boot sequence and migration timing. - id: ios-platform type: file @@ -21,7 +21,7 @@ sources: note: Defines iOS secure storage, Supabase config loading, and native service bindings. - id: ios-content type: file - path: iosApp/VitruvianPhoenix/VitruvianPhoenix/ContentView.swift + path: iosApp/PhoenixApp/PhoenixApp/ContentView.swift note: Shows that SwiftUI only hosts the shared Compose view controller. - id: android-safe-word type: file @@ -56,7 +56,7 @@ verified: 2026-06-27 --- Android and iOS are thin hosts around the shared Compose core, but they still define boot order, secure token storage, native auth and health adapters, cue and voice integrations, and workout background behavior. Future work that only reads shared code can miss real platform constraints here even when the feature logic itself lives in common Kotlin [@main-activity] [@android-platform] [@ios-app] [@ios-platform]. -Android applies the stored locale before `setContent {}` runs. `MainActivity.applyStoredLocaleBeforeComposition()` reads `vitruvian_preferences` directly and updates the platform locale so non-English users do not see an English first frame during cold start [@main-activity]. +Android applies the stored locale before `setContent {}` runs. `MainActivity.applyStoredLocaleBeforeComposition()` reads `phoenix_preferences` directly and updates the platform locale so non-English users do not see an English first frame during cold start [@main-activity]. Android gates the shared UI behind `RequireBlePermissions { AndroidAppHost() }`, sets `STREAM_MUSIC` as the volume control stream, and enables edge-to-edge layout in the native activity [@main-activity]. Workout background continuity is Android-only in this repo because the platform module binds `WorkoutServiceController` to `AndroidWorkoutServiceController` [@android-platform]. @@ -68,7 +68,7 @@ Health permission flow is another real host asymmetry. Android launches the Heal The same host boundary applies to [[workout-safety-and-feedback]]. Android workout cues route through `SoundPool` on `STREAM_MUSIC` with `MediaPlayer` fallback, and Android voice stop uses offline `SpeechRecognizer` plus transient audio focus [@android-haptics] [@android-safe-word]. iOS workout cues route through `AVAudioSession`, `AVAudioPlayer`, and UIKit haptics, while iOS voice stop depends on on-device `SFSpeechRecognizer`, separate speech and microphone permission flow, and interruption recovery after foreground or audio-session changes [@ios-haptics] [@ios-safe-word]. -iOS boot starts in Swift, not Kotlin. `VitruvianPhoenixApp` initializes Koin through `KoinInitIosKt.doInitKoin()`, runs migrations immediately afterward through `KoinInitKt.runMigrations()`, and then loads a SwiftUI `ContentView` that only wraps the shared Compose `UIViewController` [@ios-app] [@ios-content]. +iOS boot starts in Swift, not Kotlin. `PhoenixAppEntry` initializes Koin through `KoinInitIosKt.doInitKoin()`, runs migrations immediately afterward through `KoinInitKt.runMigrations()`, and then loads a SwiftUI `ContentView` that only wraps the shared Compose `UIViewController` [@ios-app] [@ios-content]. iOS secure storage uses two stores with different roles. General preferences stay in `NSUserDefaultsSettings`, while portal auth tokens are migrated into `KeychainSettings` under `com.devil.phoenixproject.auth` [@ios-platform]. Unlike Android, failed Keychain migration logs an error but does not crash the app, so re-authentication is the fallback path [@ios-platform]. diff --git a/.almanac/pages/profiles.md b/.almanac/pages/profiles.md index d2794d8a4..a30f655ab 100644 --- a/.almanac/pages/profiles.md +++ b/.almanac/pages/profiles.md @@ -9,7 +9,7 @@ sources: note: Defines the `UserProfile` model, default-profile bootstrap, active-profile state, Supabase linking, subscription fields, and delete-time reassignment behavior. - id: schema-file type: file - path: shared/src/commonMain/sqldelight/com/devil/phoenixproject/database/VitruvianDatabase.sq + path: shared/src/commonMain/sqldelight/com/devil/phoenixproject/database/PhoenixDatabase.sq note: Defines the `UserProfile` table, the `profile_id` columns that partition many tables, and the backup-sync query that exports profile rows. - id: migration-manager type: file diff --git a/.almanac/pages/project-phoenix.md b/.almanac/pages/project-phoenix.md index 269d60417..603cd1e92 100644 --- a/.almanac/pages/project-phoenix.md +++ b/.almanac/pages/project-phoenix.md @@ -1,6 +1,6 @@ --- title: Project Phoenix -summary: Project Phoenix is a Kotlin Multiplatform rescue app that restores local-first control of Vitruvian Trainer hardware after the original company's shutdown. +summary: Project Phoenix is a Kotlin Multiplatform rescue app that restores local-first control of Phoenix Trainer hardware after the original company's shutdown. topics: [product, systems] sources: - id: repo-readme @@ -22,11 +22,11 @@ sources: - id: ble-scan type: file path: shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/KableBleConnectionManager.kt - note: Defines the BLE discovery filters Phoenix uses to recognize Vitruvian devices in practice. + note: Defines the BLE discovery filters Phoenix uses to recognize Phoenix devices in practice. status: active verified: 2026-06-20 --- -Project Phoenix is a community-maintained control app for Vitruvian V-Form and Trainer+ machines after the original company closure. The repo positions the app as a way to keep the hardware usable instead of turning it into e-waste, and it ships for both Android and iOS [@repo-readme]. +Project Phoenix is a community-maintained control app for Phoenix V-Form and Trainer+ machines after the original company closure. The repo positions the app as a way to keep the hardware usable instead of turning it into e-waste, and it ships for both Android and iOS [@repo-readme]. The app is a Kotlin Multiplatform codebase with a shared `shared` module and thin `androidApp` and `iosApp` hosts. The shared module uses Compose Multiplatform, Koin, SQLDelight, Ktor, Kable, and multiplatform settings so most behavior lives once in shared code [@shared-build]. @@ -38,7 +38,7 @@ That local-first contract is why the remote pages in [[sync]] are framed as opti ## Hardware contract -Phoenix only claims support for two hardware families in current project memory: V-Form machines advertised as `Vee_*` and Trainer+ machines advertised as `VIT*` [@repo-readme]. The BLE scanner first accepts `Vee_*`, `VIT*`, and `Vitruvian*` names, then falls back to advertisements that expose the `0000fef3` service UUID, the Nordic UART service UUID, or non-empty `0000fef3` service data so devices can still be discovered when the advertisement is incomplete or nameless [@ble-scan]. +Phoenix only claims support for two hardware families in current project memory: V-Form machines advertised as `Vee_*` and Trainer+ machines advertised as `VIT*` [@repo-readme]. The BLE scanner first accepts `Vee_*`, `VIT*`, and `Phoenix*` names, then falls back to advertisements that expose the `0000fef3` service UUID, the Nordic UART service UUID, or non-empty `0000fef3` service data so devices can still be discovered when the advertisement is incomplete or nameless [@ble-scan]. The hardware contract in this repo is per-cable weight, not total machine weight. `Constants` caps the default safe UI limit at `100 kg` per cable, carries a separate `110 kg` per-cable ceiling for Trainer+, and keeps the rest of the app's stored values and calculations in per-cable units rather than total machine load [@app-constants]. @@ -55,7 +55,7 @@ The fastest rule is that most coding work should not start here. Open this page The fastest way into the code still starts from the cluster hubs unless the symptom is already narrow enough for a leaf page: - Shared app structure and screen boundaries: [[app-architecture]] -- BLE, routine flow, diagnostics, or trainer communication: [[workouts]] then [[vitruvian-ble-protocol]] if the issue is already clearly in the transport layer +- BLE, routine flow, diagnostics, or trainer communication: [[workouts]] then [[phoenix-ble-protocol]] if the issue is already clearly in the transport layer - Workout lifecycle and routine orchestration: [[workouts]] - Persistence, migrations, backups, or profile-scoped visibility: [[data]] then [[local-data-model]] if the issue is already structural - Supabase project configuration, redirect allowlists, or Edge Function surface: [[supabase]] diff --git a/.almanac/pages/routines-and-training-cycles.md b/.almanac/pages/routines-and-training-cycles.md index f51d237ed..569f966b4 100644 --- a/.almanac/pages/routines-and-training-cycles.md +++ b/.almanac/pages/routines-and-training-cycles.md @@ -9,7 +9,7 @@ sources: note: Defines active-profile-scoped routine loading, group handling, superset traversal, and routine-specific lifecycle hooks. - id: db-schema type: file - path: shared/src/commonMain/sqldelight/com/devil/phoenixproject/database/VitruvianDatabase.sq + path: shared/src/commonMain/sqldelight/com/devil/phoenixproject/database/PhoenixDatabase.sq note: Defines Routine, RoutineExercise, Superset, RoutineGroup, TrainingCycle, and related persistence columns. - id: cycle-models type: file diff --git a/.almanac/pages/supabase.md b/.almanac/pages/supabase.md index a42aa7a4d..3084b137d 100644 --- a/.almanac/pages/supabase.md +++ b/.almanac/pages/supabase.md @@ -21,7 +21,7 @@ sources: note: Shows Android build-time credential injection and the fail-fast check for missing Supabase URL or anon key. - id: android-app type: file - path: androidApp/src/main/kotlin/com/devil/phoenixproject/VitruvianApp.kt + path: androidApp/src/main/kotlin/com/devil/phoenixproject/PhoenixApp.kt note: Shows Android runtime injection of SupabaseConfig into the shared Koin graph. - id: ios-platform type: file @@ -54,7 +54,7 @@ Mobile OAuth is constrained by Supabase project configuration. `PortalAuthReposi ## Platform configuration inputs -Android and iOS inject Supabase config differently into the same shared graph. Android reads `supabase.url` and `supabase.anon.key` from `local.properties`, falls back to `SUPABASE_URL` and `SUPABASE_ANON_KEY` environment variables for CI, and refuses app builds unless values are present or `-Pskip.supabase.check=true` is set for test-only work [@android-build]. `VitruvianApp` then binds those values into Koin as `SupabaseConfig` [@android-app]. +Android and iOS inject Supabase config differently into the same shared graph. Android reads `supabase.url` and `supabase.anon.key` from `local.properties`, falls back to `SUPABASE_URL` and `SUPABASE_ANON_KEY` environment variables for CI, and refuses app builds unless values are present or `-Pskip.supabase.check=true` is set for test-only work [@android-build]. `PhoenixApp` then binds those values into Koin as `SupabaseConfig` [@android-app]. iOS loads the same values from `SUPABASE_URL` and `SUPABASE_ANON_KEY` entries in the app bundle and throws during Koin setup if either value is missing [@ios-platform]. The tracked `SupabaseBase.xcconfig` can include a local ignored `Supabase.xcconfig`, and the iOS README says GitHub Actions writes that ignored file from repository secrets during CI builds [@ios-readme]. diff --git a/.almanac/pages/theme-mode.md b/.almanac/pages/theme-mode.md index fe10bc144..8e2aedcdd 100644 --- a/.almanac/pages/theme-mode.md +++ b/.almanac/pages/theme-mode.md @@ -37,7 +37,7 @@ sources: note: Shows the legacy boolean Android wrapper is deprecated because it coerces ThemeMode.SYSTEM into light or dark. - id: ios-content type: file - path: iosApp/VitruvianPhoenix/VitruvianPhoenix/ContentView.swift + path: iosApp/PhoenixApp/PhoenixApp/ContentView.swift note: Shows SwiftUI only hosts the shared Compose controller and does not own separate theme preference state. - id: theme-plan type: file @@ -46,7 +46,7 @@ sources: status: active verified: 2026-06-22 --- -Theme selection in [[project-phoenix]] is a shared runtime setting, not a platform-host preference. `ThemeMode` is a three-state enum with `SYSTEM`, `LIGHT`, and `DARK`, and the shared `VitruvianTheme` maps `SYSTEM` to `isSystemInDarkTheme()` so the app can follow OS appearance changes without a separate Android or iOS settings model [@theme-model]. +Theme selection in [[project-phoenix]] is a shared runtime setting, not a platform-host preference. `ThemeMode` is a three-state enum with `SYSTEM`, `LIGHT`, and `DARK`, and the shared `PhoenixTheme` maps `SYSTEM` to `isSystemInDarkTheme()` so the app can follow OS appearance changes without a separate Android or iOS settings model [@theme-model]. ## Source of truth @@ -56,9 +56,9 @@ Dynamic color is a separate preference. The same view model stores `dynamic_colo ## Shared propagation path -`AppContent` is the point where theme state becomes runtime UI state. It collects `themeMode` and `dynamicColorEnabled` from `ThemeViewModel`, passes both into `VitruvianTheme`, and also passes `themeMode` plus `onThemeModeChange` into `EnhancedMainScreen` so shared settings and toolbar affordances mutate the same source of truth [@app-content]. +`AppContent` is the point where theme state becomes runtime UI state. It collects `themeMode` and `dynamicColorEnabled` from `ThemeViewModel`, passes both into `PhoenixTheme`, and also passes `themeMode` plus `onThemeModeChange` into `EnhancedMainScreen` so shared settings and toolbar affordances mutate the same source of truth [@app-content]. -The shared theme function is the load-bearing API. The older Android and iOS overloads that accept only `darkTheme: Boolean` are both marked deprecated because converting a platform signal into a boolean destroys `ThemeMode.SYSTEM` as a persisted user choice before the shared theme sees it [@android-theme-wrapper] [@ios-theme-wrapper]. Future theming work should stay on the common `VitruvianTheme(themeMode = ..., dynamicColorEnabled = ...)` path even when the change starts from a host-specific file. +The shared theme function is the load-bearing API. The older Android and iOS overloads that accept only `darkTheme: Boolean` are both marked deprecated because converting a platform signal into a boolean destroys `ThemeMode.SYSTEM` as a persisted user choice before the shared theme sees it [@android-theme-wrapper] [@ios-theme-wrapper]. Future theming work should stay on the common `PhoenixTheme(themeMode = ..., dynamicColorEnabled = ...)` path even when the change starts from a host-specific file. ## UI contract diff --git a/.almanac/pages/workout-engine.md b/.almanac/pages/workout-engine.md index 32961879a..30c22305d 100644 --- a/.almanac/pages/workout-engine.md +++ b/.almanac/pages/workout-engine.md @@ -38,4 +38,4 @@ The common characterization tests document several runtime contracts. `startWork Stopping a workout is also intentionally asymmetric. `stopWorkout(exitingWorkout = true)` returns to `Idle`, while `stopWorkout(exitingWorkout = false)` enters `SetSummary`, and a guard flag makes a second stop request a no-op while the first is still in progress [@lifecycle-tests]. -Read [[workouts]] first if the failure could still belong to the broader workout cluster instead of live session orchestration alone. Read [[routines-and-training-cycles]] next if the task is about programming workouts, [[vitruvian-ble-protocol]] if it is about trainer commands, rep counting, or packet shape changes, [[equipment-rack]] if the behavior changes with accessory load context, [[local-data-model]] if live-session behavior looks corrupted by stored routine or history state, [[gamification]] if the bug starts after workout completion rather than during a live set, or [[platform-hosts]] if Android and iOS diverge around foreground service or native lifecycle behavior. +Read [[workouts]] first if the failure could still belong to the broader workout cluster instead of live session orchestration alone. Read [[routines-and-training-cycles]] next if the task is about programming workouts, [[phoenix-ble-protocol]] if it is about trainer commands, rep counting, or packet shape changes, [[equipment-rack]] if the behavior changes with accessory load context, [[local-data-model]] if live-session behavior looks corrupted by stored routine or history state, [[gamification]] if the bug starts after workout completion rather than during a live set, or [[platform-hosts]] if Android and iOS diverge around foreground service or native lifecycle behavior. diff --git a/.almanac/pages/workouts.md b/.almanac/pages/workouts.md index 2dd3a7799..e6875bd4b 100644 --- a/.almanac/pages/workouts.md +++ b/.almanac/pages/workouts.md @@ -45,7 +45,7 @@ sources: note: Defines workout cue playback, voice-stop gating, and platform safety-feedback behavior. - id: ble-page type: file - path: .almanac/pages/vitruvian-ble-protocol.md + path: .almanac/pages/phoenix-ble-protocol.md note: Defines trainer communication, packet parsing, and command semantics. - id: diagnostics-page type: file @@ -78,15 +78,15 @@ Phoenix workout behavior is one cluster, but the code and wiki split it into nin ## Cluster map -The page links map directly onto those code boundaries: [[vitruvian-ble-protocol]] explains the BLE layer under `KableBleConnectionManager`, [[workout-engine]] explains the session state and control layer under `DefaultWorkoutSessionManager`, [[routines-and-training-cycles]] explains the editable programming layer above those runtime managers, [[machine-diagnostics]] explains the fault-snapshot and troubleshooting export path, [[workout-safety-and-feedback]] explains cue playback and voice-stop behavior around active workouts, [[strength-assessment-and-insights]] explains the local analytics layer that both consumes and informs workout data, [[equipment-rack]] explains accessory-load defaults and saved rack context, [[gamification]] explains workout-derived PR celebrations and badge state, and [[data-backup-and-repair]] explains the backup, restore, and startup-repair layer that can change workout-visible history without touching the session managers [@ble-page] [@workout-engine-page] [@routines-page] [@diagnostics-page] [@safety-page] [@assessment-page] [@rack-page] [@gamification-page] [@backup-repair-page]. +The page links map directly onto those code boundaries: [[phoenix-ble-protocol]] explains the BLE layer under `KableBleConnectionManager`, [[workout-engine]] explains the session state and control layer under `DefaultWorkoutSessionManager`, [[routines-and-training-cycles]] explains the editable programming layer above those runtime managers, [[machine-diagnostics]] explains the fault-snapshot and troubleshooting export path, [[workout-safety-and-feedback]] explains cue playback and voice-stop behavior around active workouts, [[strength-assessment-and-insights]] explains the local analytics layer that both consumes and informs workout data, [[equipment-rack]] explains accessory-load defaults and saved rack context, [[gamification]] explains workout-derived PR celebrations and badge state, and [[data-backup-and-repair]] explains the backup, restore, and startup-repair layer that can change workout-visible history without touching the session managers [@ble-page] [@workout-engine-page] [@routines-page] [@diagnostics-page] [@safety-page] [@assessment-page] [@rack-page] [@gamification-page] [@backup-repair-page]. -Read [[project-phoenix]] before this cluster when the task depends on supported hardware, per-cable load assumptions, or the repo's preservation goals after the original Vitruvian shutdown. Those product constraints explain why backward-compatible firmware handling and local-first workout behavior are treated as design requirements rather than legacy baggage. +Read [[project-phoenix]] before this cluster when the task depends on supported hardware, per-cable load assumptions, or the repo's preservation goals after the original Phoenix shutdown. Those product constraints explain why backward-compatible firmware handling and local-first workout behavior are treated as design requirements rather than legacy baggage. Read [[frontend]] before this cluster when the symptom is clearly in shared screen ownership, route placement, or Compose state projection and you still do not know which workout leaf page owns the underlying behavior. ## Default read order -Read this cluster in dependency order when the bug source is unclear. [[vitruvian-ble-protocol]] explains what the machine sends and accepts, [[workout-engine]] explains how shared runtime state reacts to that data, [[machine-diagnostics]] explains the live fault and crash snapshot branch under the same BLE connection, [[workout-safety-and-feedback]] explains cue and safe-stop behavior wrapped around that session runtime, [[routines-and-training-cycles]] explains how pre-authored plans feed the live session, [[equipment-rack]] explains how accessory context modifies or annotates that flow, [[gamification]] explains how saved workouts turn into PR or badge state, and [[strength-assessment-and-insights]] explains the local 1RM and analytics layer that feeds percentage-based programming and exercise detail history [@ble-page] [@workout-engine-page] [@diagnostics-page] [@safety-page] [@routines-page] [@rack-page] [@gamification-page] [@assessment-page]. +Read this cluster in dependency order when the bug source is unclear. [[phoenix-ble-protocol]] explains what the machine sends and accepts, [[workout-engine]] explains how shared runtime state reacts to that data, [[machine-diagnostics]] explains the live fault and crash snapshot branch under the same BLE connection, [[workout-safety-and-feedback]] explains cue and safe-stop behavior wrapped around that session runtime, [[routines-and-training-cycles]] explains how pre-authored plans feed the live session, [[equipment-rack]] explains how accessory context modifies or annotates that flow, [[gamification]] explains how saved workouts turn into PR or badge state, and [[strength-assessment-and-insights]] explains the local 1RM and analytics layer that feeds percentage-based programming and exercise detail history [@ble-page] [@workout-engine-page] [@diagnostics-page] [@safety-page] [@routines-page] [@rack-page] [@gamification-page] [@assessment-page]. `MainViewModel` is the UI entry point, not the system boundary. [[app-architecture]] explains that screens mostly talk to one shared façade, but `MainViewModel` itself composes manager-owned flows instead of implementing workout behavior directly, so debugging usually belongs in one of the lower pages in this cluster rather than in the screen tree [@app-architecture-page] [@main-viewmodel] [@workout-engine-page]. @@ -94,7 +94,7 @@ Read this cluster in dependency order when the bug source is unclear. [[vitruvia ## Choose the leaf page -Use [[vitruvian-ble-protocol]] first for scan behavior, reconnects, packet parsing, mode IDs, command formats, or trainer firmware mismatches [@ble-page]. +Use [[phoenix-ble-protocol]] first for scan behavior, reconnects, packet parsing, mode IDs, command formats, or trainer firmware mismatches [@ble-page]. Use [[workout-engine]] first for countdowns, stop-state transitions, set-ready behavior, rest timers, active session state, and command timing [@workout-engine-page]. diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index bdaa75adb..487d1a29f 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -3,7 +3,7 @@ github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username -ko_fi: vitruvianredux +ko_fi: phoenixredux tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username diff --git a/.github/scripts/agent-quality-gate.ps1 b/.github/scripts/agent-quality-gate.ps1 index 5dc9c949f..a506e6dd6 100644 --- a/.github/scripts/agent-quality-gate.ps1 +++ b/.github/scripts/agent-quality-gate.ps1 @@ -29,7 +29,7 @@ if (-not (Test-AndroidSdkConfigured)) { "-Pskip.supabase.check=true" ` "spotlessCheck" ` "validateSchemaManifest" ` - ":shared:verifyCommonMainVitruvianDatabaseMigration" ` + ":shared:verifyCommonMainPhoenixDatabaseMigration" ` ":shared:testAndroidHostTest" ` "--console=plain" ` "--no-daemon" diff --git a/.github/scripts/agent-quality-gate.sh b/.github/scripts/agent-quality-gate.sh index 129991d87..045a5e8b7 100644 --- a/.github/scripts/agent-quality-gate.sh +++ b/.github/scripts/agent-quality-gate.sh @@ -16,7 +16,7 @@ gradle_args=( -Pskip.supabase.check=true spotlessCheck validateSchemaManifest - :shared:verifyCommonMainVitruvianDatabaseMigration + :shared:verifyCommonMainPhoenixDatabaseMigration :shared:testAndroidHostTest --console=plain --no-daemon diff --git a/.github/scripts/validate-ios-schema.sh b/.github/scripts/validate-ios-schema.sh index 267dd3b32..9e186f14c 100644 --- a/.github/scripts/validate-ios-schema.sh +++ b/.github/scripts/validate-ios-schema.sh @@ -2,11 +2,11 @@ # # Schema Manifest Validator # ========================= -# Validates that SchemaManifest.kt table definitions match VitruvianDatabase.sq +# Validates that SchemaManifest.kt table definitions match PhoenixDatabase.sq # # Background: SchemaManifest.kt provides cross-platform schema reconciliation # (Layer 4 defense against migration gaps). The table schemas in SchemaManifest.kt -# MUST exactly match VitruvianDatabase.sq, otherwise fresh installs will crash with +# MUST exactly match PhoenixDatabase.sq, otherwise fresh installs will crash with # SQLiteException when SQLDelight queries try to access columns that don't exist. # # This script validates: @@ -23,7 +23,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Go up two levels from .github/scripts to project root PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")" -SQ_FILE="$PROJECT_ROOT/shared/src/commonMain/sqldelight/com/devil/phoenixproject/database/VitruvianDatabase.sq" +SQ_FILE="$PROJECT_ROOT/shared/src/commonMain/sqldelight/com/devil/phoenixproject/database/PhoenixDatabase.sq" MANIFEST_FILE="$PROJECT_ROOT/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/local/SchemaManifest.kt" echo "Schema Manifest Validator" @@ -205,7 +205,7 @@ if errors: print(f" ERROR: {error}") print("") print("To fix: Update shared/src/commonMain/kotlin/.../SchemaManifest.kt") - print(" to match shared/src/commonMain/sqldelight/.../VitruvianDatabase.sq") + print(" to match shared/src/commonMain/sqldelight/.../PhoenixDatabase.sq") print("") print("See archived planning notes for issue-223 iOS fresh-install SQLite crash if needed") sys.exit(1) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index a12567677..1200d3fe3 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -65,7 +65,7 @@ jobs: ${{ runner.os }}-gradle- - name: Verify SQLDelight migrations - run: ./gradlew :shared:verifyCommonMainVitruvianDatabaseMigration --continue -Pskip.supabase.check=true + run: ./gradlew :shared:verifyCommonMainPhoenixDatabaseMigration --continue -Pskip.supabase.check=true - name: Run shared module unit tests run: ./gradlew :shared:testAndroidHostTest --continue -Pskip.supabase.check=true diff --git a/.github/workflows/ios-release-ipa.yml b/.github/workflows/ios-release-ipa.yml index d1fe4e0ca..542da914a 100644 --- a/.github/workflows/ios-release-ipa.yml +++ b/.github/workflows/ios-release-ipa.yml @@ -25,8 +25,8 @@ concurrency: env: BUNDLE_ID: com.devil.phoenixproject.projectphoenix - SCHEME: VitruvianPhoenix - PROJECT_PATH: iosApp/VitruvianPhoenix/VitruvianPhoenix.xcodeproj + SCHEME: PhoenixApp + PROJECT_PATH: iosApp/PhoenixApp/PhoenixApp.xcodeproj GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx8g -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g" jobs: @@ -44,7 +44,7 @@ jobs: SUPABASE_URL: ${{ secrets.SUPABASE_URL }} SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }} run: | - mkdir -p iosApp/VitruvianPhoenix/Config + mkdir -p iosApp/PhoenixApp/Config # xcconfig treats `//` as an inline comment, so URLs like `https://foo` # must be written as `https:/$()/foo` (empty xcconfig substitution) or # the value gets truncated to `https:` at parse time. Apply to both @@ -54,7 +54,7 @@ jobs: { echo "SUPABASE_URL = $url_escaped" echo "SUPABASE_ANON_KEY = $key_escaped" - } > iosApp/VitruvianPhoenix/Config/Supabase.xcconfig + } > iosApp/PhoenixApp/Config/Supabase.xcconfig - name: Verify target release env: @@ -88,10 +88,10 @@ jobs: # Update Xcode project with new build number sed -i '' "s/CURRENT_PROJECT_VERSION = [0-9][0-9]*/CURRENT_PROJECT_VERSION = $BUILD_NUM/g" \ - iosApp/VitruvianPhoenix/VitruvianPhoenix.xcodeproj/project.pbxproj + iosApp/PhoenixApp/PhoenixApp.xcodeproj/project.pbxproj # Show what was set - grep "CURRENT_PROJECT_VERSION" iosApp/VitruvianPhoenix/VitruvianPhoenix.xcodeproj/project.pbxproj | head -2 + grep "CURRENT_PROJECT_VERSION" iosApp/PhoenixApp/PhoenixApp.xcodeproj/project.pbxproj | head -2 - name: Build shared framework and resources run: | @@ -117,7 +117,7 @@ jobs: # directory to prevent auto-inclusion that flattens locale folders and # causes .cvr filename collisions. A Run Script Build Phase copies them # to the bundle preserving directory structure. - RESOURCE_DST="iosApp/VitruvianPhoenix/compose-resources" + RESOURCE_DST="iosApp/PhoenixApp/compose-resources" rm -rf "$RESOURCE_DST" mkdir -p "$RESOURCE_DST" @@ -201,7 +201,7 @@ jobs: -scheme "$SCHEME" \ -configuration Release \ -destination 'generic/platform=iOS' \ - -archivePath $RUNNER_TEMP/VitruvianPhoenix.xcarchive \ + -archivePath $RUNNER_TEMP/PhoenixApp.xcarchive \ DEVELOPMENT_TEAM="${{ secrets.TEAM_ID }}" \ CODE_SIGN_STYLE=Manual \ CODE_SIGN_IDENTITY="Apple Distribution" \ @@ -210,7 +210,7 @@ jobs: - name: Export .ipa run: | xcodebuild -exportArchive \ - -archivePath $RUNNER_TEMP/VitruvianPhoenix.xcarchive \ + -archivePath $RUNNER_TEMP/PhoenixApp.xcarchive \ -exportPath $RUNNER_TEMP/export \ -exportOptionsPlist iosApp/ExportOptions.plist diff --git a/.github/workflows/ios-testflight-internal.yml b/.github/workflows/ios-testflight-internal.yml index 490229ae6..ccb45110a 100644 --- a/.github/workflows/ios-testflight-internal.yml +++ b/.github/workflows/ios-testflight-internal.yml @@ -12,8 +12,8 @@ concurrency: env: BUNDLE_ID: com.devil.phoenixproject.projectphoenix - SCHEME: VitruvianPhoenix - PROJECT_PATH: iosApp/VitruvianPhoenix/VitruvianPhoenix.xcodeproj + SCHEME: PhoenixApp + PROJECT_PATH: iosApp/PhoenixApp/PhoenixApp.xcodeproj GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx8g -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g" jobs: @@ -29,7 +29,7 @@ jobs: SUPABASE_URL: ${{ secrets.SUPABASE_URL }} SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }} run: | - mkdir -p iosApp/VitruvianPhoenix/Config + mkdir -p iosApp/PhoenixApp/Config # xcconfig treats `//` as an inline comment, so URLs like `https://foo` # must be written as `https:/$()/foo` (empty xcconfig substitution) or # the value gets truncated to `https:` at parse time. Apply to both @@ -39,7 +39,7 @@ jobs: { echo "SUPABASE_URL = $url_escaped" echo "SUPABASE_ANON_KEY = $key_escaped" - } > iosApp/VitruvianPhoenix/Config/Supabase.xcconfig + } > iosApp/PhoenixApp/Config/Supabase.xcconfig - name: Set up Java 17 uses: actions/setup-java@v4 @@ -69,10 +69,10 @@ jobs: # Update Xcode project with new build number sed -i '' "s/CURRENT_PROJECT_VERSION = [0-9][0-9]*/CURRENT_PROJECT_VERSION = $BUILD_NUM/g" \ - iosApp/VitruvianPhoenix/VitruvianPhoenix.xcodeproj/project.pbxproj + iosApp/PhoenixApp/PhoenixApp.xcodeproj/project.pbxproj # Show what was set - grep "CURRENT_PROJECT_VERSION" iosApp/VitruvianPhoenix/VitruvianPhoenix.xcodeproj/project.pbxproj | head -2 + grep "CURRENT_PROJECT_VERSION" iosApp/PhoenixApp/PhoenixApp.xcodeproj/project.pbxproj | head -2 - name: Build shared framework and resources run: | @@ -95,12 +95,12 @@ jobs: # are lost during static linking. They must be in the app bundle root. # # Resources are placed OUTSIDE the Xcode fileSystemSynchronizedGroups - # directory (at iosApp/VitruvianPhoenix/compose-resources/, not inside - # VitruvianPhoenix/VitruvianPhoenix/) to prevent auto-inclusion that + # directory (at iosApp/PhoenixApp/compose-resources/, not inside + # PhoenixApp/PhoenixApp/) to prevent auto-inclusion that # flattens locale folders and causes .cvr filename collisions. # A Run Script Build Phase in the Xcode project copies them to the # bundle preserving directory structure. - RESOURCE_DST="iosApp/VitruvianPhoenix/compose-resources" + RESOURCE_DST="iosApp/PhoenixApp/compose-resources" rm -rf "$RESOURCE_DST" mkdir -p "$RESOURCE_DST" @@ -187,7 +187,7 @@ jobs: -scheme "$SCHEME" \ -configuration Release \ -destination 'generic/platform=iOS' \ - -archivePath $RUNNER_TEMP/VitruvianPhoenix.xcarchive \ + -archivePath $RUNNER_TEMP/PhoenixApp.xcarchive \ DEVELOPMENT_TEAM="${{ secrets.TEAM_ID }}" \ CODE_SIGN_STYLE=Manual \ CODE_SIGN_IDENTITY="Apple Distribution" \ @@ -196,7 +196,7 @@ jobs: - name: Export .ipa run: | xcodebuild -exportArchive \ - -archivePath $RUNNER_TEMP/VitruvianPhoenix.xcarchive \ + -archivePath $RUNNER_TEMP/PhoenixApp.xcarchive \ -exportPath $RUNNER_TEMP/export \ -exportOptionsPlist iosApp/ExportOptions.plist @@ -210,9 +210,9 @@ jobs: mkdir -p ~/.appstoreconnect/private_keys echo "$APPSTORE_API_KEY" > ~/.appstoreconnect/private_keys/AuthKey_${APPSTORE_API_KEY_ID}.p8 - IPA_PATH="$RUNNER_TEMP/export/VitruvianPhoenix.ipa" + IPA_PATH="$RUNNER_TEMP/export/PhoenixApp.ipa" BUILD_VERSION="${{ steps.build_number.outputs.build_number }}" - MARKETING_VERSION=$(grep 'MARKETING_VERSION' iosApp/VitruvianPhoenix/VitruvianPhoenix.xcodeproj/project.pbxproj | head -1 | sed 's/.*= //;s/;.*//') + MARKETING_VERSION=$(grep 'MARKETING_VERSION' iosApp/PhoenixApp/PhoenixApp.xcodeproj/project.pbxproj | head -1 | sed 's/.*= //;s/;.*//') echo "=== Validating IPA before upload ===" echo "IPA: $IPA_PATH ($(du -h "$IPA_PATH" | cut -f1))" diff --git a/.github/workflows/ios-testflight.yml b/.github/workflows/ios-testflight.yml index 738b2f17c..890c655f2 100644 --- a/.github/workflows/ios-testflight.yml +++ b/.github/workflows/ios-testflight.yml @@ -25,8 +25,8 @@ concurrency: env: BUNDLE_ID: com.devil.phoenixproject.projectphoenix - SCHEME: VitruvianPhoenix - PROJECT_PATH: iosApp/VitruvianPhoenix/VitruvianPhoenix.xcodeproj + SCHEME: PhoenixApp + PROJECT_PATH: iosApp/PhoenixApp/PhoenixApp.xcodeproj GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx8g -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g" jobs: @@ -44,7 +44,7 @@ jobs: SUPABASE_URL: ${{ secrets.SUPABASE_URL }} SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }} run: | - mkdir -p iosApp/VitruvianPhoenix/Config + mkdir -p iosApp/PhoenixApp/Config # xcconfig treats `//` as an inline comment, so URLs like `https://foo` # must be written as `https:/$()/foo` (empty xcconfig substitution) or # the value gets truncated to `https:` at parse time. Apply to both @@ -54,7 +54,7 @@ jobs: { echo "SUPABASE_URL = $url_escaped" echo "SUPABASE_ANON_KEY = $key_escaped" - } > iosApp/VitruvianPhoenix/Config/Supabase.xcconfig + } > iosApp/PhoenixApp/Config/Supabase.xcconfig - name: Set up Java 17 uses: actions/setup-java@v4 @@ -85,10 +85,10 @@ jobs: # Update Xcode project with new build number # [0-9][0-9]* requires at least one digit to avoid matching empty strings sed -i '' "s/CURRENT_PROJECT_VERSION = [0-9][0-9]*/CURRENT_PROJECT_VERSION = $BUILD_NUM/g" \ - iosApp/VitruvianPhoenix/VitruvianPhoenix.xcodeproj/project.pbxproj + iosApp/PhoenixApp/PhoenixApp.xcodeproj/project.pbxproj # Show what was set - grep "CURRENT_PROJECT_VERSION" iosApp/VitruvianPhoenix/VitruvianPhoenix.xcodeproj/project.pbxproj | head -2 + grep "CURRENT_PROJECT_VERSION" iosApp/PhoenixApp/PhoenixApp.xcodeproj/project.pbxproj | head -2 - name: Build shared framework and resources run: | @@ -111,12 +111,12 @@ jobs: # are lost during static linking. They must be in the app bundle root. # # Resources are placed OUTSIDE the Xcode fileSystemSynchronizedGroups - # directory (at iosApp/VitruvianPhoenix/compose-resources/, not inside - # VitruvianPhoenix/VitruvianPhoenix/) to prevent auto-inclusion that + # directory (at iosApp/PhoenixApp/compose-resources/, not inside + # PhoenixApp/PhoenixApp/) to prevent auto-inclusion that # flattens locale folders and causes .cvr filename collisions. # A Run Script Build Phase in the Xcode project copies them to the # bundle preserving directory structure. - RESOURCE_DST="iosApp/VitruvianPhoenix/compose-resources" + RESOURCE_DST="iosApp/PhoenixApp/compose-resources" rm -rf "$RESOURCE_DST" mkdir -p "$RESOURCE_DST" @@ -203,7 +203,7 @@ jobs: -scheme "$SCHEME" \ -configuration Release \ -destination 'generic/platform=iOS' \ - -archivePath $RUNNER_TEMP/VitruvianPhoenix.xcarchive \ + -archivePath $RUNNER_TEMP/PhoenixApp.xcarchive \ DEVELOPMENT_TEAM="${{ secrets.TEAM_ID }}" \ CODE_SIGN_STYLE=Manual \ CODE_SIGN_IDENTITY="Apple Distribution" \ @@ -212,7 +212,7 @@ jobs: - name: Export .ipa run: | xcodebuild -exportArchive \ - -archivePath $RUNNER_TEMP/VitruvianPhoenix.xcarchive \ + -archivePath $RUNNER_TEMP/PhoenixApp.xcarchive \ -exportPath $RUNNER_TEMP/export \ -exportOptionsPlist iosApp/ExportOptions.plist @@ -226,9 +226,9 @@ jobs: mkdir -p ~/.appstoreconnect/private_keys echo "$APPSTORE_API_KEY" > ~/.appstoreconnect/private_keys/AuthKey_${APPSTORE_API_KEY_ID}.p8 - IPA_PATH="$RUNNER_TEMP/export/VitruvianPhoenix.ipa" + IPA_PATH="$RUNNER_TEMP/export/PhoenixApp.ipa" BUILD_VERSION="${{ steps.build_number.outputs.build_number }}" - MARKETING_VERSION=$(grep 'MARKETING_VERSION' iosApp/VitruvianPhoenix/VitruvianPhoenix.xcodeproj/project.pbxproj | head -1 | sed 's/.*= //;s/;.*//') + MARKETING_VERSION=$(grep 'MARKETING_VERSION' iosApp/PhoenixApp/PhoenixApp.xcodeproj/project.pbxproj | head -1 | sed 's/.*= //;s/;.*//') echo "=== Validating IPA before upload ===" echo "IPA: $IPA_PATH ($(du -h "$IPA_PATH" | cut -f1))" diff --git a/.github/workflows/release-all.yml b/.github/workflows/release-all.yml index bde6aae16..632e5d131 100644 --- a/.github/workflows/release-all.yml +++ b/.github/workflows/release-all.yml @@ -58,7 +58,7 @@ jobs: run: | set -euo pipefail ANDROID_VERSION="$(sed -nE 's/^[[:space:]]*versionName = "([^"]+)".*/\1/p' androidApp/build.gradle.kts | head -1)" - IOS_VERSIONS="$(sed -nE 's/^[[:space:]]*MARKETING_VERSION = ([^;]+);/\1/p' iosApp/VitruvianPhoenix/VitruvianPhoenix.xcodeproj/project.pbxproj | sort -u)" + IOS_VERSIONS="$(sed -nE 's/^[[:space:]]*MARKETING_VERSION = ([^;]+);/\1/p' iosApp/PhoenixApp/PhoenixApp.xcodeproj/project.pbxproj | sort -u)" if ! [[ "$ANDROID_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.]+)?$ ]]; then echo "Invalid Android versionName: '$ANDROID_VERSION'." >&2 exit 1 diff --git a/.gitignore b/.gitignore index fa3599061..3dded7d4b 100644 --- a/.gitignore +++ b/.gitignore @@ -18,10 +18,10 @@ build/ # Local configuration local.properties *.local.properties -# fix(audit): C1 — cover Supabase.xcconfig in any location under VitruvianPhoenix +# fix(audit): C1 — cover Supabase.xcconfig in any location under PhoenixApp # so a stray sibling file doesn't leak the anon key again. -iosApp/VitruvianPhoenix/**/Supabase.xcconfig -iosApp/VitruvianPhoenix/Config/Supabase.xcconfig +iosApp/PhoenixApp/**/Supabase.xcconfig +iosApp/PhoenixApp/Config/Supabase.xcconfig # Kotlin .kotlin/ @@ -39,8 +39,8 @@ iosApp/**/*.xcworkspace/ **/*.xcuserstate **/project.xcworkspace/xcuserdata/ iosApp/**/DerivedData/ -iosApp/VitruvianPhoenix/compose-resources/ -iosApp/VitruvianPhoenix/VitruvianPhoenix/compose-resources/ +iosApp/PhoenixApp/compose-resources/ +iosApp/PhoenixApp/PhoenixApp/compose-resources/ # Desktop /out/ diff --git a/.phoenix-review/BUG-FIX-PLAN.md b/.phoenix-review/BUG-FIX-PLAN.md index 5720bf042..118dd44af 100644 --- a/.phoenix-review/BUG-FIX-PLAN.md +++ b/.phoenix-review/BUG-FIX-PLAN.md @@ -285,7 +285,7 @@ - F297 | impact=265 | Platform | medium / bug | file=shared/src/iosMain/kotlin/com/devil/phoenixproject/util/DeviceInfo.ios.kt | lines= | `toJson()` interpolates bundle/device strings directly into JSON. Device names and bundle values can contain quotes, backslashes, or newlines, producing invalid JSON. | fix: Use kotlinx.serialization or at least a shared JSON string escaping helper for all string values. - F169 | impact=260 | Domain | medium / error | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/domain/usecase/TemplateConverter.kt | lines=116-118 | A non-rest `CycleDayTemplate` with `routine == null` throws via `error("Training day ... has no routine")`. Template conversion is typically user/content driven, so a malformed or partially loaded template can crash the caller instead of returning a warning or partial conversion result. | fix: Treat missing training-day routines as recoverable conversion warnings. Add the day name/number to warnings, skip or create an empty placeholder day, and avoid throwing from normal template validation failures. - F078 | impact=255 | Data | medium / failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/data/sync/PortalTokenStorage.kt | lines=52 and 69-72 | The class documentation says tokens must be stored in secure settings, but the constructor accepts any `Settings` instance and `verifyStorageIntegrity()` only verifies read/write behavior. A future DI/test/wiring mistake can pass plaintext `Settings` and still pass verification, silently storing JWTs and refresh tokens unencrypted. | fix: make the constructor accept a distinct secure-storage wrapper/type (for example an expect/actual `SecureTokenStorage` or qualified provider object), keep direct construction internal, or add platform-specific verification/marker checks so plaintext settings cannot satisfy the token-storage dependency in production. -- F082 | impact=355 | Data | high / failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/KableBleConnectionManager.kt | lines=245-297, 469-479 | `startScanning()` recognizes Vitruvian devices by advertised name, service UUID, or FEF3 service data. `scanAndConnect()` only accepts devices with a non-null name beginning with `Vee_` or `VIT`. Devices that are discoverable through the repository's manual scan path can therefore never be found by the auto-connect path. | fix: Extract one shared Vitruvian advertisement predicate and use it in both `startScanning()` and `scanAndConnect()`. Preserve the nameless/service-data fallback when auto-connecting. +- F082 | impact=355 | Data | high / failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/KableBleConnectionManager.kt | lines=245-297, 469-479 | `startScanning()` recognizes Phoenix devices by advertised name, service UUID, or FEF3 service data. `scanAndConnect()` only accepts devices with a non-null name beginning with `Vee_` or `VIT`. Devices that are discoverable through the repository's manual scan path can therefore never be found by the auto-connect path. | fix: Extract one shared Phoenix advertisement predicate and use it in both `startScanning()` and `scanAndConnect()`. Preserve the nameless/service-data fallback when auto-connecting. - F083 | impact=255 | Data | medium / failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/KableBleConnectionManager.kt | lines=763-850 | `onDeviceReady()` reads `p.services.value` once. If it is null, the code logs a warning but still starts notifications and polling immediately. On platforms where service discovery populates slightly after `State.Connected`, this can attempt characteristic operations before discovery is complete and create intermittent connection/setup failures. | fix: Wait with a bounded timeout for `p.services` to become non-null and to contain required characteristics before starting observers/polling. If readiness fails, surface a retryable initialization error instead of continuing with partially discovered GATT state. - F087 | impact=255 | Data | medium / failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/DiagnosticFaultDecoder.kt | lines=84-92 | `decodeFlaggedFault` drops unknown fault bits whenever at least one known bit is also present. For example, a fault word with a known motor bit plus an undocumented high bit will display only the known label, hiding evidence of an additional active fault. | fix: Track the union of known masks, compute `unknownBits = code and knownMask.inv()`, and append an `Unknown bits 0x....` label whenever unknown bits are non-zero. - F089 | impact=255 | Data | medium / failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/DiscoMode.kt | lines=76-84 | `stop()` cancels `discoJob` and immediately launches the restore-color command without waiting for the old job to finish. If the old job is in the middle of a BLE write when cancellation occurs, a late disco color write can race with and override the restore command. | fix: Serialize disco writes through the same job or a mutex, and cancel-and-join the cycling job before sending the restore command. @@ -356,7 +356,7 @@ - F310 | impact=220 | Utilities | medium / stub | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/HardwareDetection.kt | lines=37-43, 52-62 | `getCapabilities(deviceName)` ignores its input and always returns `HardwareCapabilities.DEFAULT`, which enables Echo and eccentric mode and sets `maxResistanceKg = 200f` for every device. This is placeholder capability logic; it can both over-enable features on unsupported devices and under-represent Trainer+ resistance while giving callers no indication that the result is assumed rather than detected. | fix: Return an explicit unknown/assumed capability state, wire the VERSION/firmware characteristic into capability detection, and make capability consumers handle unknowns conservatively instead of treating defaults as detected hardware facts. - F312 | impact=220 | Utilities | medium / stub | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/HealthPermissionSettingsLauncher.kt | lines=6-13 | The common API promises a bridge for opening Phoenix's health-permissions settings, and the KDoc specifically says recovery requires sending the user to the Health Connect app-permissions screen. The API returns `Unit` and has no success/failure callback, so callers cannot tell when the platform implementation cannot open anything. This matters because the iOS actual is currently a no-op and Android settings intents can also fail if the target package/activity is unavailable, leaving the user in the same recovery state with no fallback UI. | fix: Make the contract report whether settings were actually launched, for example `openSettings(): Boolean` or `openSettings(onResult: (Boolean) -> Unit)`, and require unsupported platforms to return/report `false` so the caller can show manual instructions. - F343 | impact=175 | Data | low / bug | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/data/sync/GoTrueModels.kt | lines=35-38 | `GoTrueUser.displayName` reads `user_metadata["display_name"]` via `JsonElement.toString().trim('"')`. That returns JSON text, not decoded content, so names with escaped quotes, unicode escapes, or non-string values can display incorrectly (for example `Jane \"JJ\" Doe` rather than `Jane "JJ" Doe`). | fix: read `jsonPrimitive.contentOrNull` after checking the element is a primitive string, and ignore or safely stringify non-string metadata. -- F348 | impact=175 | Data | low / bug | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/DiagnosticFaultDecoder.kt | lines=50-55, 87-90 | Vitruvian fault flags `8` and `16` both use the label `Message failure`, and the final `.distinct()` collapses them. If both bits are set, the decoded label reports only one message failure and loses which bit(s) were active. | fix: Give the two flags distinct labels if the protocol differentiates them, or include the bit mask in duplicate labels so combined faults remain diagnosable. +- F348 | impact=175 | Data | low / bug | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/DiagnosticFaultDecoder.kt | lines=50-55, 87-90 | Phoenix fault flags `8` and `16` both use the label `Message failure`, and the final `.distinct()` collapses them. If both bits are set, the decoded label reports only one message failure and loses which bit(s) were active. | fix: Give the two flags distinct labels if the protocol differentiates them, or include the bit mask in duplicate labels so combined faults remain diagnosable. - F350 | impact=175 | Data | low / bug | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/MetricPollingEngine.kt | lines=264-267 | The explicit `TimeoutCancellationException` catch path increments `consecutiveTimeouts` and delays, but it never checks `MAX_CONSECUTIVE_TIMEOUTS` or invokes `onConnectionLost()`. Although the main `withTimeoutOrNull` path handles null timeouts, any timeout exception reaching this catch can avoid the disconnect threshold indefinitely. | fix: Reuse the same threshold check as the null-timeout branch after incrementing `consecutiveTimeouts`, or remove the catch if `withTimeoutOrNull` is the only expected timeout path. - F368 | impact=170 | Domain | low / bug | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/domain/premium/ReadinessEngine.kt | lines=118-122 | The ACWR sweet-spot comment says the 0.8 and 1.3 edges should score around 70, but the formula uses a single divisor of `0.3f`. At ACWR 0.8 the score is about 80, not 70, so low-end readiness is overstated relative to the documented policy. | fix: Use asymmetric scaling from the peak: divide by `0.2f` on the 0.8-1.0 side and by `0.3f` on the 1.0-1.3 side, or update the documented score zones/tests if 80 at 0.8 is intentional. - F370 | impact=170 | Domain | low / bug | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/domain/premium/SmartSuggestionsEngine.kt | lines=263-305 | `TimeOfDayAnalysis.windowVolumes` is populated with `windowAvgIntensity` rather than volume. The model name promises volume, but the engine stores average kg-per-rep intensity, creating a contract mismatch for any future consumer that reads the field as volume. | fix: Rename the model field to `windowAvgIntensity`, or populate `windowVolumes` with actual total volume and add a separate intensity field if needed. @@ -435,12 +435,12 @@ - F397 | impact=135 | Presentation | low / failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/viewmodel/ThemeViewModel.kt | lines=32-45, 62-70 | Theme setters update state before writing settings and do not catch persistence failures. If settings storage throws, the UI can show the new theme/dynamic-color state even though it was not persisted for the next launch. | fix: Persist first or roll back state on failure, and expose a small error event/log path so preference-write problems are not silent. - F398 | impact=135 | Presentation | low / failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/ui/theme/Material3Expressive.kt | lines=21, 29, 37 | The exported expressive motion specs are hard-coded as `spring()`, while the comments describe them as reusable for buttons, cards, toggles, and other interactions. Compose animation specs are type-specific, so these values cannot be passed directly to common non-Float animations such as `Dp`, `Offset`, `Int`, or color/shape-driven transitions. This is not currently crashing because the search found no active call sites, but it is a likely integration trap when the shared theme API starts being reused. | fix: Expose generic factory functions such as `fun expressiveSpringDefault(): SpringSpec` or provide explicitly named specs per animated type, and keep the current Float specs private or clearly named `FloatSpringDefault`. - F399 | impact=135 | Presentation | low / failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/ui/theme/Theme.kt | lines=68, 74 | `secondaryContainer` and `tertiaryContainer` in the light color scheme are stored as semi-transparent colors via `.copy(alpha = ...)`. Material color scheme roles are consumed globally by components as semantic container colors; making the role itself translucent means the final displayed color depends on whatever surface happens to be behind the component. That can produce inconsistent visuals under nested cards, dialogs, gradients, or future dynamic theme changes even if the current white-background contrast is acceptable. | fix: Pre-composite these colors against the intended light surface/background and store opaque color tokens in the `ColorScheme`, reserving alpha adjustments for local component-level overlays where the backing surface is known. -- F400 | impact=135 | Presentation | low / failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/ui/theme/ThemeHelpers.kt | lines=24-35 | `screenBackgroundBrush()` infers dark mode by checking `MaterialTheme.colorScheme.background.luminance() < 0.5f`. This duplicates theme-mode logic indirectly and can choose the wrong gradient if a future dynamic/custom scheme uses an unusually bright dark background, a dim light background, or a transitional/system-provided palette near the threshold. The helper already controls prominent screen backgrounds, so a misclassification would be visible across many screens. | fix: Pass the resolved `useDarkColors`/`ThemeMode` into the helper or provide a `CompositionLocal` for the resolved dark flag from `VitruvianTheme`, rather than deriving it from a single color's luminance. +- F400 | impact=135 | Presentation | low / failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/ui/theme/ThemeHelpers.kt | lines=24-35 | `screenBackgroundBrush()` infers dark mode by checking `MaterialTheme.colorScheme.background.luminance() < 0.5f`. This duplicates theme-mode logic indirectly and can choose the wrong gradient if a future dynamic/custom scheme uses an unusually bright dark background, a dim light background, or a transitional/system-provided palette near the threshold. The helper already controls prominent screen backgrounds, so a misclassification would be visible across many screens. | fix: Pass the resolved `useDarkColors`/`ThemeMode` into the helper or provide a `CompositionLocal` for the resolved dark flag from `PhoenixTheme`, rather than deriving it from a single color's luminance. - F403 | impact=135 | Platform | low / stub | file=androidApp/src/main/kotlin/com/devil/phoenixproject/ui/theme/AndroidTheme.kt | lines=17-28 | This Android theme wrapper is deprecated because it collapses `ThemeMode.SYSTEM` into a concrete dark/light boolean before delegating to the shared theme. Although current app entry appears to use the shared `AppContent` path, keeping this wrapper callable leaves a known footgun for future Android UI code. | fix: Remove the deprecated wrapper once callers are migrated, or replace it with an overload that accepts and forwards `ThemeMode` directly so system-theme semantics are preserved. - F424 | impact=132 | Build & CI | low / stub | file=.github/FUNDING.yml | lines=3-15 | The file still contains the generated placeholder funding entries/comments for every platform except Ko-fi. The unused null keys are not harmful YAML, but they are template residue and make the funding configuration look incomplete. | fix: Remove unused placeholder keys and keep only configured funding providers, or fill in the intended accounts/URLs. - F430 | impact=132 | DI & presentation utilities | low / failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/util/WeightDisplayFormatter.kt | lines=52-56 | `formatNumeric()` decides whether to drop decimals using exact float modulo (`display % 1f == 0f`) and then converts to `Int`. Values that are mathematically whole but represented as `54.999996f` will display as `55.0` instead of `55`, and non-finite/out-of-range values would be converted to misleading integer text rather than rejected. | fix: Use finite-value checks and epsilon-based whole-number detection before integer formatting, e.g. round to one decimal first, compare within tolerance, and avoid `toInt()` for non-finite values. - F417 | impact=130 | Utilities | low / failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/DataBackupManager.kt | lines=2568-2576, 2635-2644 | Auto-backup filenames concatenate raw `sessionId` and `routineSessionId` into filesystem paths. IDs are expected to be generated internally, but restored or externally-sourced IDs can contain path separators or other invalid filename characters. That can make backup creation fail or write outside the intended filename shape. | fix: Sanitize IDs before using them in filenames (allow a conservative `[A-Za-z0-9._-]` set, replace everything else) while preserving the original IDs inside the JSON payload. -- F419 | impact=130 | Utilities | low / failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/BleConstants.kt | lines=49-51 | The device-name constants are internally inconsistent and stale. `DEVICE_NAME_PREFIX` is `"Vee"`, while `DEVICE_NAME_PATTERN` is `"^Vitruvian.*$"`; the active scanner/hardware detection code also treats `"Vee_"` and `"VIT"` as Vitruvian names. Any future code that relies on `DEVICE_NAME_PATTERN` would reject the same devices that the scanner currently accepts. | fix: Replace the single regex with one canonical matcher shared by scanning and hardware detection, e.g. covering `Vee_`, `VIT`, and `Vitruvian`, or remove unused/stale constants so future filters cannot drift. +- F419 | impact=130 | Utilities | low / failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/BleConstants.kt | lines=49-51 | The device-name constants are internally inconsistent and stale. `DEVICE_NAME_PREFIX` is `"Vee"`, while `DEVICE_NAME_PATTERN` is `"^Phoenix.*$"`; the active scanner/hardware detection code also treats `"Vee_"` and `"VIT"` as Phoenix names. Any future code that relies on `DEVICE_NAME_PATTERN` would reject the same devices that the scanner currently accepts. | fix: Replace the single regex with one canonical matcher shared by scanning and hardware detection, e.g. covering `Vee_`, `VIT`, and `Phoenix`, or remove unused/stale constants so future filters cannot drift. - F421 | impact=130 | Utilities | low / failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/HealthPermissionRequester.kt | lines=8-13 | The common declaration still documents iOS as a no-op (`iOS currently no-ops because HealthKit is not wired up yet`), but the current iOS actual implementation injects `HealthIntegration` and calls `requestPermissions()`. This stale contract is a low-level integration hazard: callers and future reviewers can incorrectly assume iOS permission requests never run, add duplicate platform workarounds, or skip handling the real asynchronous iOS permission result. | fix: Update the common KDoc to describe the current platform contract rather than historical implementation status. If platform support can vary, document the expected callback semantics for success, denial, cancellation, and unsupported platforms. - F422 | impact=130 | Utilities | low / failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/KmpUtils.kt | lines=159-168 | `formatRelativeTimestamp()` does not handle future timestamps. When `timestamp > now`, `diffMs` and `diffMinutes` become negative and the first branch (`diffMinutes < 1`) returns `Just now`. Clock skew, imported external activity data, scheduled records, or server-provided timestamps can therefore be mislabeled instead of shown as future or invalid. | fix: Add an explicit future-time branch before the existing past-time thresholds, such as returning `In Xm`, `In Xh`, or falling back to an absolute date for any negative difference beyond a small clock-skew tolerance. - F423 | impact=130 | Utilities | low / failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/Locking.kt | lines=3-7 | The common contract and KDoc imply that the `lock` argument controls the critical section and that Native may fall back to direct execution. Current platform behavior is not that simple: Android uses the supplied monitor, while iOS uses a single global lock and ignores the supplied `lock`. Callers that assume two different lock objects allow independent progress will behave differently across platforms, potentially causing unexpected serialization/jank on iOS or insufficient documentation around lock ordering. | fix: Update the common KDoc to state the exact cross-platform semantics and limitations. If per-object locking is required by callers, introduce a platform abstraction that guarantees per-lock isolation on every target; otherwise rename/document this as a global/native-compatible serialization helper. diff --git a/.phoenix-review/CONSOLIDATED-REPORT.md b/.phoenix-review/CONSOLIDATED-REPORT.md index ef94a7a0c..e10efe1fb 100644 --- a/.phoenix-review/CONSOLIDATED-REPORT.md +++ b/.phoenix-review/CONSOLIDATED-REPORT.md @@ -99,7 +99,7 @@ - F079 | module=Data | category=failure-point | file=shared/src/iosMain/kotlin/com/devil/phoenixproject/data/auth/OAuth.ios.kt | lines=135-138 | description: `PresentationContextProvider` returns the first connected `UIWindowScene` or creates a new `UIWindow()` when none is found. In multi-scene/iPad, background, or cold-start edge cases, the first scene may not be foreground active/key, and a newly-created unattached window is not a valid presentation anchor. `ASWebAuthenticationSession` can fail to start or present behind the wrong scene. | fix: select a foreground-active `UIWindowScene` and its key window, falling back only to an existing visible window. If no presentation anchor exists, fail with a clear error instead of returning a detached `UIWindow()`. - F080 | module=Data | category=bug | file=shared/src/iosMain/kotlin/com/devil/phoenixproject/data/auth/OAuth.ios.kt | lines=50-54 and 106-113 | description: iOS `OAuthLauncher.launch()` allows overlapping launches. A second call overwrites the shared `session` and `presentationProvider` properties while the first coroutine/session may still be active. Unlike Android's `AndroidOAuthBridge.beginFlow()`, there is no cancellation or rejection of the previous flow, so rapid double taps or parallel Google/Apple sign-in requests can produce orphaned sessions or resume the wrong caller. | fix: guard `launch()` with a mutex/in-flight flag. Either reject a second launch with a clear failure or cancel the existing `ASWebAuthenticationSession` before starting a new one, matching Android's single-flight behavior. - F081 | module=Data | category=failure-point | file=Notification observer jobs are untracked and can be duplicated across reconnect/readiness cycles | lines=584-601, 850-955, 882-912, 915-950 | description: `State.Connected` launches `onDeviceReady()`, which calls `startObservingNotifications()`. That method launches REPS, VERSION, and MODE observation coroutines into the long-lived repository scope but does not retain job handles or cancel previous observers before starting new ones. Repeated `Connected` emissions or rapid reconnects can leave duplicate collectors on the same peripheral or stale collectors from prior peripherals. | fix: Store notification observer jobs in the connection manager, cancel them before starting a new observer set, and cancel them during disconnect/cleanup/shutdown. Consider guarding `onDeviceReady()` so each peripheral generation initializes only once. -- F082 | module=Data | category=failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/KableBleConnectionManager.kt | lines=245-297, 469-479 | description: `startScanning()` recognizes Vitruvian devices by advertised name, service UUID, or FEF3 service data. `scanAndConnect()` only accepts devices with a non-null name beginning with `Vee_` or `VIT`. Devices that are discoverable through the repository's manual scan path can therefore never be found by the auto-connect path. | fix: Extract one shared Vitruvian advertisement predicate and use it in both `startScanning()` and `scanAndConnect()`. Preserve the nameless/service-data fallback when auto-connecting. +- F082 | module=Data | category=failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/KableBleConnectionManager.kt | lines=245-297, 469-479 | description: `startScanning()` recognizes Phoenix devices by advertised name, service UUID, or FEF3 service data. `scanAndConnect()` only accepts devices with a non-null name beginning with `Vee_` or `VIT`. Devices that are discoverable through the repository's manual scan path can therefore never be found by the auto-connect path. | fix: Extract one shared Phoenix advertisement predicate and use it in both `startScanning()` and `scanAndConnect()`. Preserve the nameless/service-data fallback when auto-connecting. - F083 | module=Data | category=failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/KableBleConnectionManager.kt | lines=763-850 | description: `onDeviceReady()` reads `p.services.value` once. If it is null, the code logs a warning but still starts notifications and polling immediately. On platforms where service discovery populates slightly after `State.Connected`, this can attempt characteristic operations before discovery is complete and create intermittent connection/setup failures. | fix: Wait with a bounded timeout for `p.services` to become non-null and to contain required characteristics before starting observers/polling. If readiness fails, surface a retryable initialization error instead of continuing with partially discovered GATT state. - F084 | module=Data | category=failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/data/repository/KableBleRepository.kt | lines=216-233 | description: The repository-level `startWorkout(params)` constructs a 4-byte packet manually (`0x02, mode, weightLow, weightHigh`) and starts active polling on success. Elsewhere the codebase uses `BlePacketFactory.createProgramParams()` / `createEchoControl()` for the full 96-byte/32-byte protocol. Any caller using this interface method can start a workout with an incomplete/legacy packet that does not carry reps, warmup, progression, Echo settings, or current protocol fields. | fix: Either remove/deprecate this interface method if all real starts are routed elsewhere, or implement it by delegating to the same validated packet factory path used by active session code. - F085 | module=Data | category=failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/manager/BleConnectionManager.kt | lines=160-187 | description: `startScanning()` launches `bleRepository.startScanning()` and ignores its `Result`; `connectToDevice()` launches `bleRepository.connect(device)` and ignores its `Result`. Failures can be logged by the lower layer but never reflected in `_connectionError`, leaving the UI without actionable feedback for scan/connect failures. | fix: Check returned `Result` values in these launched coroutines and update `_connectionError` or route to the existing failure callback path. Also catch non-cancellation exceptions around these non-init launches for Kotlin/Native safety. @@ -368,7 +368,7 @@ - F345 | module=Data | category=failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/BleOperationQueue.kt | lines=44-75 | description: The retry loop is `for (attempt in 0 until maxRetries)`. If a caller passes `maxRetries <= 0`, no BLE write is attempted and the function returns `IllegalStateException("Unknown write error")`, hiding the real configuration problem. | fix: Validate `maxRetries >= 1` at entry and fail fast with an `IllegalArgumentException`, or coerce to at least one attempt. - F346 | module=Data | category=error | file=Polling API/documentation mismatch around heuristic polling during bodyweight/rest mode | lines=225-233 | description: The interface comment says `stopMonitorPollingOnly()` should stop "monitor and heuristic polling" while keeping diagnostic polling and heartbeat running. The implementation path in `MetricPollingEngine.stopMonitorOnly()` cancels only `monitorPollingJob` and leaves heuristic polling active. That mismatch makes it unclear whether heuristic reads are intentionally kept as connection activity or accidentally continue during modes that should not emit workout telemetry. | fix: Decide the intended behavior. If heuristic polling should stop, cancel `heuristicPollingJob` in `stopMonitorOnly()`. If it should continue, update the repository contract comment and any callers/tests that assume heuristic data stops. - F347 | module=Data | category=failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/BleOperationQueue.kt | lines=19, 31, 81-96 | description: The file warns that Kotlin `Mutex` is not reentrant, while also exposing `withLock()` for compound operations plus separate `read()`, `write()`, and `writeSimple()` methods that acquire the same mutex. A future compound operation that calls one of these helpers inside `withLock()` will deadlock. The contract is documented but not enforced. | fix: Keep the compound locked API internal/narrow, add debug owner checks, or provide unlocked lower-level helpers explicitly intended for use inside `withLock()` so nested acquisition is not needed. -- F348 | module=Data | category=bug | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/DiagnosticFaultDecoder.kt | lines=50-55, 87-90 | description: Vitruvian fault flags `8` and `16` both use the label `Message failure`, and the final `.distinct()` collapses them. If both bits are set, the decoded label reports only one message failure and loses which bit(s) were active. | fix: Give the two flags distinct labels if the protocol differentiates them, or include the bit mask in duplicate labels so combined faults remain diagnosable. +- F348 | module=Data | category=bug | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/DiagnosticFaultDecoder.kt | lines=50-55, 87-90 | description: Phoenix fault flags `8` and `16` both use the label `Message failure`, and the final `.distinct()` collapses them. If both bits are set, the decoded label reports only one message failure and loses which bit(s) were active. | fix: Give the two flags distinct labels if the protocol differentiates them, or include the bit mask in duplicate labels so combined faults remain diagnosable. - F349 | module=Data | category=failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/HandleStateDetector.kt | lines=371-394 | description: Baseline-relative grab/release checks only compare `position - baseline` against positive thresholds. A setup or calibration where meaningful handle movement is represented as a negative delta from baseline will not be recognized as grabbed, while release checks may be overly permissive for large negative movement. | fix: Confirm the protocol sign convention for all pulley/cable orientations; if either direction can represent extension, compare `abs(position - baseline)` for relative movement or track direction per handle. - F350 | module=Data | category=bug | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/MetricPollingEngine.kt | lines=264-267 | description: The explicit `TimeoutCancellationException` catch path increments `consecutiveTimeouts` and delays, but it never checks `MAX_CONSECUTIVE_TIMEOUTS` or invokes `onConnectionLost()`. Although the main `withTimeoutOrNull` path handles null timeouts, any timeout exception reaching this catch can avoid the disconnect threshold indefinitely. | fix: Reuse the same threshold check as the null-timeout branch after incrementing `consecutiveTimeouts`, or remove the catch if `withTimeoutOrNull` is the only expected timeout path. - F351 | module=Data | category=stub | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/MonitorDataProcessor.kt | lines=236-250 | description: `calculateRawVelocity()` is now unused after firmware velocity became authoritative. Keeping this stale private implementation increases the chance of future code accidentally reintroducing client-side velocity behavior that contradicts the current parser comments and tests. | fix: Remove the dead function or mark it as test-only/reference documentation outside the production hot path. @@ -420,7 +420,7 @@ - F397 | module=Presentation | category=failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/viewmodel/ThemeViewModel.kt | lines=32-45, 62-70 | description: Theme setters update state before writing settings and do not catch persistence failures. If settings storage throws, the UI can show the new theme/dynamic-color state even though it was not persisted for the next launch. | fix: Persist first or roll back state on failure, and expose a small error event/log path so preference-write problems are not silent. - F398 | module=Presentation | category=failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/ui/theme/Material3Expressive.kt | lines=21, 29, 37 | description: The exported expressive motion specs are hard-coded as `spring()`, while the comments describe them as reusable for buttons, cards, toggles, and other interactions. Compose animation specs are type-specific, so these values cannot be passed directly to common non-Float animations such as `Dp`, `Offset`, `Int`, or color/shape-driven transitions. This is not currently crashing because the search found no active call sites, but it is a likely integration trap when the shared theme API starts being reused. | fix: Expose generic factory functions such as `fun expressiveSpringDefault(): SpringSpec` or provide explicitly named specs per animated type, and keep the current Float specs private or clearly named `FloatSpringDefault`. - F399 | module=Presentation | category=failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/ui/theme/Theme.kt | lines=68, 74 | description: `secondaryContainer` and `tertiaryContainer` in the light color scheme are stored as semi-transparent colors via `.copy(alpha = ...)`. Material color scheme roles are consumed globally by components as semantic container colors; making the role itself translucent means the final displayed color depends on whatever surface happens to be behind the component. That can produce inconsistent visuals under nested cards, dialogs, gradients, or future dynamic theme changes even if the current white-background contrast is acceptable. | fix: Pre-composite these colors against the intended light surface/background and store opaque color tokens in the `ColorScheme`, reserving alpha adjustments for local component-level overlays where the backing surface is known. -- F400 | module=Presentation | category=failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/ui/theme/ThemeHelpers.kt | lines=24-35 | description: `screenBackgroundBrush()` infers dark mode by checking `MaterialTheme.colorScheme.background.luminance() < 0.5f`. This duplicates theme-mode logic indirectly and can choose the wrong gradient if a future dynamic/custom scheme uses an unusually bright dark background, a dim light background, or a transitional/system-provided palette near the threshold. The helper already controls prominent screen backgrounds, so a misclassification would be visible across many screens. | fix: Pass the resolved `useDarkColors`/`ThemeMode` into the helper or provide a `CompositionLocal` for the resolved dark flag from `VitruvianTheme`, rather than deriving it from a single color's luminance. +- F400 | module=Presentation | category=failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/ui/theme/ThemeHelpers.kt | lines=24-35 | description: `screenBackgroundBrush()` infers dark mode by checking `MaterialTheme.colorScheme.background.luminance() < 0.5f`. This duplicates theme-mode logic indirectly and can choose the wrong gradient if a future dynamic/custom scheme uses an unusually bright dark background, a dim light background, or a transitional/system-provided palette near the threshold. The helper already controls prominent screen backgrounds, so a misclassification would be visible across many screens. | fix: Pass the resolved `useDarkColors`/`ThemeMode` into the helper or provide a `CompositionLocal` for the resolved dark flag from `PhoenixTheme`, rather than deriving it from a single color's luminance. - F401 | module=Platform | category=failure-point | file=androidApp/src/main/kotlin/com/devil/phoenixproject/MainActivity.kt | lines=50-55 | description: The pre-API-33 locale branch mutates `resources.configuration` directly and updates only this activity's resources. This can leave application-context resources and other context wrappers temporarily out of sync during startup, especially because Koin singletons and shared code can resolve resources outside the activity. | fix: Clone the configuration before mutation (`Configuration(resources.configuration)`) and centralize locale application so both activity and application/base contexts use the same locale source. Prefer the AndroidX/AppCompat per-app-locale path if available for API 26-32. - F402 | module=Platform | category=failure-point | file=androidApp/src/main/kotlin/com/devil/phoenixproject/auth/OAuthRedirectActivity.kt | lines=83-99 | description: `routeBackToApp()` runs after every handled intent, including missing-data cancellation and arbitrary explicit launches. This lets any caller foreground `MainActivity` through the exported redirect activity even when no OAuth flow is active. | fix: Route back only when a valid pending OAuth flow was completed or cancelled by this activity. If no flow is pending or the URI is invalid, finish silently after logging. - F403 | module=Platform | category=stub | file=androidApp/src/main/kotlin/com/devil/phoenixproject/ui/theme/AndroidTheme.kt | lines=17-28 | description: This Android theme wrapper is deprecated because it collapses `ThemeMode.SYSTEM` into a concrete dark/light boolean before delegating to the shared theme. Although current app entry appears to use the shared `AppContent` path, keeping this wrapper callable leaves a known footgun for future Android UI code. | fix: Remove the deprecated wrapper once callers are migrated, or replace it with an overload that accepts and forwards `ThemeMode` directly so system-theme semantics are preserved. @@ -439,7 +439,7 @@ - F416 | module=Platform | category=failure-point | file=shared/src/iosMain/kotlin/com/devil/phoenixproject/util/ScreenUtils.ios.kt | lines= | description: `UIApplication.sharedApplication.setIdleTimerDisabled(enabled)` is a UIKit application mutation. If called from a background coroutine/thread, it can violate UIKit main-thread expectations. | fix: Dispatch this call to the main queue or require callers to invoke it from the main thread. - F417 | module=Utilities | category=failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/DataBackupManager.kt | lines=2568-2576, 2635-2644 | description: Auto-backup filenames concatenate raw `sessionId` and `routineSessionId` into filesystem paths. IDs are expected to be generated internally, but restored or externally-sourced IDs can contain path separators or other invalid filename characters. That can make backup creation fail or write outside the intended filename shape. | fix: Sanitize IDs before using them in filenames (allow a conservative `[A-Za-z0-9._-]` set, replace everything else) while preserving the original IDs inside the JSON payload. - F418 | module=Utilities | category=error | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/DataBackupManager.kt | lines=503-515, 807-820, 859-871, 1621-1644, 1706-1727 | description: Several import counters treat successful execution of `INSERT OR IGNORE` queries as an import even when SQLite ignored the row. `tryImport()` returns `Unit` on both inserted and ignored rows, so duplicate routine groups, earned badges, and session notes can be counted as imported rather than skipped. This produces misleading restore summaries and hides duplicate/conflict conditions from the UI. | fix: Check affected-row counts where available, query for existing IDs before insert, or use insert/upsert APIs that can distinguish inserted vs ignored rows before updating `Imported`/`Skipped` counters. -- F419 | module=Utilities | category=failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/BleConstants.kt | lines=49-51 | description: The device-name constants are internally inconsistent and stale. `DEVICE_NAME_PREFIX` is `"Vee"`, while `DEVICE_NAME_PATTERN` is `"^Vitruvian.*$"`; the active scanner/hardware detection code also treats `"Vee_"` and `"VIT"` as Vitruvian names. Any future code that relies on `DEVICE_NAME_PATTERN` would reject the same devices that the scanner currently accepts. | fix: Replace the single regex with one canonical matcher shared by scanning and hardware detection, e.g. covering `Vee_`, `VIT`, and `Vitruvian`, or remove unused/stale constants so future filters cannot drift. +- F419 | module=Utilities | category=failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/BleConstants.kt | lines=49-51 | description: The device-name constants are internally inconsistent and stale. `DEVICE_NAME_PREFIX` is `"Vee"`, while `DEVICE_NAME_PATTERN` is `"^Phoenix.*$"`; the active scanner/hardware detection code also treats `"Vee_"` and `"VIT"` as Phoenix names. Any future code that relies on `DEVICE_NAME_PATTERN` would reject the same devices that the scanner currently accepts. | fix: Replace the single regex with one canonical matcher shared by scanning and hardware detection, e.g. covering `Vee_`, `VIT`, and `Phoenix`, or remove unused/stale constants so future filters cannot drift. - F420 | module=Utilities | category=bug | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/Constants.kt | lines=69-79 | description: `UnitConverter.formatDecimal()` truncates toward zero rather than formatting to the nearest one decimal place, and it loses the sign for negative fractional values between `-1` and `0`. Verified examples following the implementation: `1.99 -> 1.9`, `0.19 -> 0.1`, `-0.5 -> 0.5`, and `22.0462 -> 22`. This can under-report converted weights and produce misleading display text. | fix: Use a real rounding/formatting path (`round(value * 10) / 10`, locale-stable formatting, or a DecimalFormat/Multiplatform equivalent) and preserve the sign for negative values. - F421 | module=Utilities | category=failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/HealthPermissionRequester.kt | lines=8-13 | description: The common declaration still documents iOS as a no-op (`iOS currently no-ops because HealthKit is not wired up yet`), but the current iOS actual implementation injects `HealthIntegration` and calls `requestPermissions()`. This stale contract is a low-level integration hazard: callers and future reviewers can incorrectly assume iOS permission requests never run, add duplicate platform workarounds, or skip handling the real asynchronous iOS permission result. | fix: Update the common KDoc to describe the current platform contract rather than historical implementation status. If platform support can vary, document the expected callback semantics for success, denial, cancellation, and unsupported platforms. - F422 | module=Utilities | category=failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/KmpUtils.kt | lines=159-168 | description: `formatRelativeTimestamp()` does not handle future timestamps. When `timestamp > now`, `diffMs` and `diffMinutes` become negative and the first branch (`diffMinutes < 1`) returns `Just now`. Clock skew, imported external activity data, scheduled records, or server-provided timestamps can therefore be mislabeled instead of shown as future or invalid. | fix: Add an explicit future-time branch before the existing past-time thresholds, such as returning `In Xm`, `In Xh`, or falling back to an absolute date for any negative difference beyond a small clock-skew tolerance. diff --git a/.phoenix-review/android-app-main.md b/.phoenix-review/android-app-main.md index ba3fe0bfc..6cc0b381c 100644 --- a/.phoenix-review/android-app-main.md +++ b/.phoenix-review/android-app-main.md @@ -2,7 +2,7 @@ Scope reviewed: - `androidApp/src/main/kotlin/com/devil/phoenixproject/MainActivity.kt` -- `androidApp/src/main/kotlin/com/devil/phoenixproject/VitruvianApp.kt` +- `androidApp/src/main/kotlin/com/devil/phoenixproject/PhoenixApp.kt` - `androidApp/src/main/kotlin/com/devil/phoenixproject/auth/OAuthRedirectActivity.kt` - `androidApp/src/main/kotlin/com/devil/phoenixproject/service/WorkoutForegroundService.kt` - `androidApp/src/main/kotlin/com/devil/phoenixproject/ui/theme/AndroidTheme.kt` @@ -39,7 +39,7 @@ Category breakdown: - Description: The pre-API-33 locale branch mutates `resources.configuration` directly and updates only this activity's resources. This can leave application-context resources and other context wrappers temporarily out of sync during startup, especially because Koin singletons and shared code can resolve resources outside the activity. - Suggested fix direction: Clone the configuration before mutation (`Configuration(resources.configuration)`) and centralize locale application so both activity and application/base contexts use the same locale source. Prefer the AndroidX/AppCompat per-app-locale path if available for API 26-32. -## `androidApp/src/main/kotlin/com/devil/phoenixproject/VitruvianApp.kt` +## `androidApp/src/main/kotlin/com/devil/phoenixproject/PhoenixApp.kt` No findings in the reviewed file. diff --git a/.phoenix-review/data---ble-part-1.md b/.phoenix-review/data---ble-part-1.md index dd1866b00..a02b7e5cb 100644 --- a/.phoenix-review/data---ble-part-1.md +++ b/.phoenix-review/data---ble-part-1.md @@ -33,8 +33,8 @@ Assigned files reviewed directly when present. Several assigned paths do not exi - Line numbers: 245-297, 469-479 - Category: failure-point - Severity: medium -- Description: `startScanning()` recognizes Vitruvian devices by advertised name, service UUID, or FEF3 service data. `scanAndConnect()` only accepts devices with a non-null name beginning with `Vee_` or `VIT`. Devices that are discoverable through the repository's manual scan path can therefore never be found by the auto-connect path. -- Suggested fix direction: Extract one shared Vitruvian advertisement predicate and use it in both `startScanning()` and `scanAndConnect()`. Preserve the nameless/service-data fallback when auto-connecting. +- Description: `startScanning()` recognizes Phoenix devices by advertised name, service UUID, or FEF3 service data. `scanAndConnect()` only accepts devices with a non-null name beginning with `Vee_` or `VIT`. Devices that are discoverable through the repository's manual scan path can therefore never be found by the auto-connect path. +- Suggested fix direction: Extract one shared Phoenix advertisement predicate and use it in both `startScanning()` and `scanAndConnect()`. Preserve the nameless/service-data fallback when auto-connecting. ### 3. Notification observer jobs are untracked and can be duplicated across reconnect/readiness cycles diff --git a/.phoenix-review/data---ble-part-2.md b/.phoenix-review/data---ble-part-2.md index a49de475d..1a8e51a13 100644 --- a/.phoenix-review/data---ble-part-2.md +++ b/.phoenix-review/data---ble-part-2.md @@ -32,7 +32,7 @@ Scope reviewed from task t_fa884bd4. Repository path on disk is `/Users/christop - Category: bug - Severity: low - Line numbers: 50-55, 87-90 -- Description: Vitruvian fault flags `8` and `16` both use the label `Message failure`, and the final `.distinct()` collapses them. If both bits are set, the decoded label reports only one message failure and loses which bit(s) were active. +- Description: Phoenix fault flags `8` and `16` both use the label `Message failure`, and the final `.distinct()` collapses them. If both bits are set, the decoded label reports only one message failure and loses which bit(s) were active. - Suggested fix direction: Give the two flags distinct labels if the protocol differentiates them, or include the bit mask in duplicate labels so combined faults remain diagnosable. ## shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/DiscoMode.kt diff --git a/.phoenix-review/ui-theme-&-styling.md b/.phoenix-review/ui-theme-&-styling.md index 4e1a15f3e..996e4b1a0 100644 --- a/.phoenix-review/ui-theme-&-styling.md +++ b/.phoenix-review/ui-theme-&-styling.md @@ -80,7 +80,7 @@ No findings. - Severity: low - Line numbers: 24-35 - Description: `screenBackgroundBrush()` infers dark mode by checking `MaterialTheme.colorScheme.background.luminance() < 0.5f`. This duplicates theme-mode logic indirectly and can choose the wrong gradient if a future dynamic/custom scheme uses an unusually bright dark background, a dim light background, or a transitional/system-provided palette near the threshold. The helper already controls prominent screen backgrounds, so a misclassification would be visible across many screens. -- Suggested fix direction: Pass the resolved `useDarkColors`/`ThemeMode` into the helper or provide a `CompositionLocal` for the resolved dark flag from `VitruvianTheme`, rather than deriving it from a single color's luminance. +- Suggested fix direction: Pass the resolved `useDarkColors`/`ThemeMode` into the helper or provide a `CompositionLocal` for the resolved dark flag from `PhoenixTheme`, rather than deriving it from a single color's luminance. ## `shared/src/commonMain/kotlin/com/devil/phoenixproject/ui/theme/Type.kt` diff --git a/.phoenix-review/utilities-part-2.md b/.phoenix-review/utilities-part-2.md index 6d4196654..4510d8fa1 100644 --- a/.phoenix-review/utilities-part-2.md +++ b/.phoenix-review/utilities-part-2.md @@ -26,8 +26,8 @@ Findings: 11 total - Category: failure-point - Severity: low - Line numbers: 49-51 -- Description: The device-name constants are internally inconsistent and stale. `DEVICE_NAME_PREFIX` is `"Vee"`, while `DEVICE_NAME_PATTERN` is `"^Vitruvian.*$"`; the active scanner/hardware detection code also treats `"Vee_"` and `"VIT"` as Vitruvian names. Any future code that relies on `DEVICE_NAME_PATTERN` would reject the same devices that the scanner currently accepts. -- Suggested fix direction: Replace the single regex with one canonical matcher shared by scanning and hardware detection, e.g. covering `Vee_`, `VIT`, and `Vitruvian`, or remove unused/stale constants so future filters cannot drift. +- Description: The device-name constants are internally inconsistent and stale. `DEVICE_NAME_PREFIX` is `"Vee"`, while `DEVICE_NAME_PATTERN` is `"^Phoenix.*$"`; the active scanner/hardware detection code also treats `"Vee_"` and `"VIT"` as Phoenix names. Any future code that relies on `DEVICE_NAME_PATTERN` would reject the same devices that the scanner currently accepts. +- Suggested fix direction: Replace the single regex with one canonical matcher shared by scanning and hardware detection, e.g. covering `Vee_`, `VIT`, and `Phoenix`, or remove unused/stale constants so future filters cannot drift. ### `shared/src/commonMain/kotlin/com/devil/phoenixproject/util/BlePacketFactory.kt` diff --git a/.superpowers/sdd/task-3.7-report.md b/.superpowers/sdd/task-3.7-report.md index c5f693432..1b9812954 100644 --- a/.superpowers/sdd/task-3.7-report.md +++ b/.superpowers/sdd/task-3.7-report.md @@ -84,7 +84,7 @@ Verified zero `ButtonDefaults.` usages remain in the file (grep returned no matc **Strings audit:** - `disconnect_title` = "Disconnect?" already exists and is used in `WorkoutTab.kt`. **Reused** for the title (minor text change: "Disconnect Device?" → "Disconnect?"; functionally equivalent). -- `disconnect_message` = "Are you sure you want to disconnect from the Vitruvian machine?" — lacks a `%1$s` placeholder for `deviceName`, and is in use in `WorkoutTab.kt`. **Not repurposed.** +- `disconnect_message` = "Are you sure you want to disconnect from the Phoenix machine?" — lacks a `%1$s` placeholder for `deviceName`, and is in use in `WorkoutTab.kt`. **Not repurposed.** - **Minted** `disconnect_message_device` = "Are you sure you want to disconnect from %1$s?" in `values/strings.xml` only; 5 locale files fall back to English. **Keys reused:** `disconnect_title` diff --git a/ANDROID_INSTALL.md b/ANDROID_INSTALL.md index 6a93197d3..b9b2ab919 100644 --- a/ANDROID_INSTALL.md +++ b/ANDROID_INSTALL.md @@ -56,7 +56,7 @@ Android requires permission to install apps from outside the Play Store. When you first launch the app, you'll be asked to grant permissions: ### Bluetooth Permissions -- **Nearby devices** - Required to scan for and connect to your Vitruvian trainer +- **Nearby devices** - Required to scan for and connect to your Phoenix trainer - Tap **Allow** when prompted ### Location Permission @@ -89,7 +89,7 @@ When a new version is released: - Ensure Bluetooth is enabled on your device - Make sure you granted Bluetooth/Nearby devices permission - On Android 11 and below, ensure Location is enabled (required for BLE scanning) -- Move closer to your Vitruvian trainer +- Move closer to your Phoenix trainer - Try turning your trainer off and on again ### App Crashes on Launch @@ -137,10 +137,10 @@ A: Yes, as long as it has Bluetooth Low Energy support and runs Android 8.0+. **Q: Why does it need location permission?** A: Android requires location permission for Bluetooth scanning on Android 11 and below. This is a platform limitation, not something we can change. The app never accesses your actual location. -**Q: What Vitruvian devices are supported?** +**Q: What Phoenix devices are supported?** A: -- Vitruvian V-Form Trainer (VIT-200) - devices starting with `Vee_` -- Vitruvian Trainer+ - devices starting with `VIT` +- Phoenix V-Form Trainer (VIT-200) - devices starting with `Vee_` +- Phoenix Trainer+ - devices starting with `VIT` --- diff --git a/CLAUDE.md b/CLAUDE.md index 7116b5bbb..2f32f55da 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,7 +28,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -Kotlin Multiplatform app for controlling Vitruvian Trainer workout machines via BLE. Community rescue project to keep machines functional after company bankruptcy. +Kotlin Multiplatform app for controlling Phoenix Trainer workout machines via BLE. Community rescue project to keep machines functional after company bankruptcy. ## Build Commands @@ -81,7 +81,7 @@ Nordic UART Service UUIDs in `BleInterfaces.kt`: Device names start with `Vee_` (V-Form) or `VIT` (Trainer+). ### Database Schema -SQLDelight schema at `shared/src/commonMain/sqldelight/.../VitruvianDatabase.sq`: +SQLDelight schema at `shared/src/commonMain/sqldelight/.../PhoenixDatabase.sq`: - **WorkoutSession** - Exercise sessions with mode, weight, reps - **MetricSample** - Real-time metrics (position, velocity, load, power) - **PersonalRecord** - PR tracking with 1RM calculations @@ -108,8 +108,8 @@ Located in `shared/src/commonMain/kotlin/com/devil/phoenixproject/domain/model/` - Coroutines 1.10.2 ## Hardware Support -- **Vitruvian V-Form Trainer** (VIT-200): 200kg max, device name `Vee_*` -- **Vitruvian Trainer+**: 220kg max +- **Phoenix V-Form Trainer** (VIT-200): 200kg max, device name `Vee_*` +- **Phoenix Trainer+**: 220kg max ## Sync Architecture diff --git a/README.md b/README.md index f62ac068a..6c5a558a1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Project Phoenix — Vitruvian Trainer Control App +# Project Phoenix — Phoenix Trainer Control App [![Latest Release](https://img.shields.io/github/v/release/9thLevelSoftware/Project-Phoenix-MP)](https://github.com/9thLevelSoftware/Project-Phoenix-MP/releases/latest) [![License](https://img.shields.io/badge/license-Proprietary-red.svg)](LICENSE) @@ -6,7 +6,7 @@ [![Platform](https://img.shields.io/badge/platform-Android%20%7C%20iOS-green.svg)](https://github.com/9thLevelSoftware/Project-Phoenix-MP/releases) [![DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/9thLevelSoftware/Project-Phoenix-MP) -**Keep your Vitruvian Trainer alive.** This community-developed app restores full functionality to Vitruvian V-Form and Trainer+ machines after the company's closure. Don't let your investment become e-waste. +**Keep your Phoenix Trainer alive.** This community-developed app restores full functionality to Phoenix V-Form and Trainer+ machines after the company's closure. Don't let your investment become e-waste. --- @@ -14,7 +14,7 @@ If Project Phoenix has helped keep your machine running, please consider supporting continued development: -**[☕ Support on Ko-fi](https://ko-fi.com/vitruvianredux)** +**[☕ Support on Ko-fi](https://ko-fi.com/phoenixredux)** Your support helps cover development, testing, and platform costs and keeps this community rescue project going. @@ -95,8 +95,8 @@ Current release: **[v0.9.6](https://github.com/9thLevelSoftware/Project-Phoenix- | Machine | Device Name | Max Resistance | Status | |---------|-------------|----------------|--------| -| **Vitruvian V-Form Trainer** (VIT-200) | `Vee_*` | 200 kg (440 lbs) | ✅ Fully Supported | -| **Vitruvian Trainer+** | `VIT*` | 220 kg (485 lbs) | ✅ Fully Supported | +| **Phoenix V-Form Trainer** (VIT-200) | `Vee_*` | 200 kg (440 lbs) | ✅ Fully Supported | +| **Phoenix Trainer+** | `VIT*` | 220 kg (485 lbs) | ✅ Fully Supported | --- @@ -131,7 +131,7 @@ Recent highlights: ### iOS ```bash ./gradlew :shared:assembleXCFramework -open iosApp/VitruvianPhoenix/VitruvianPhoenix.xcodeproj +open iosApp/PhoenixApp/PhoenixApp.xcodeproj ``` --- @@ -181,7 +181,7 @@ Please open an issue before starting large changes so the approach can be coordi - **Issues**: [GitHub Issues](https://github.com/9thLevelSoftware/Project-Phoenix-MP/issues) - **Discussions**: [GitHub Discussions](https://github.com/9thLevelSoftware/Project-Phoenix-MP/discussions) - **Project Portal**: [phoenix-portal.com](https://phoenix-portal.com) -- **Support Development**: [Ko-fi](https://ko-fi.com/vitruvianredux) +- **Support Development**: [Ko-fi](https://ko-fi.com/phoenixredux) --- @@ -193,12 +193,12 @@ Proprietary License - All Rights Reserved. See [LICENSE](LICENSE) file for detai ## Acknowledgments -- Original [VitruvianProjectPhoenix](https://github.com/DasBluEyedDevil/VitruvianProjectPhoenix) Android app +- Original [ProjectPhoenix](https://github.com/9thLevelSoftware/Project-Phoenix-MP) Android app - Community protocol documentation for the machine BLE interface -- Vitruvian machine owners community for testing and feedback +- Phoenix machine owners community for testing and feedback - JetBrains for Kotlin Multiplatform - All contributors and supporters --- -*Project Phoenix is a community rescue project to keep Vitruvian Trainer machines functional. It is not affiliated with or endorsed by Vitruvian.* +*Project Phoenix is a community rescue project to keep Phoenix Trainer machines functional. It is not affiliated with or endorsed by Phoenix.* diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index c9bd6641d..fdcdea952 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -319,6 +319,7 @@ android { applicationId = "com.devil.phoenixproject" minSdk = 26 targetSdk = 37 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" // Fail fast if CI injects an invalid version code instead of silently shipping a default. versionCode = injectedVersionCode ?: 5 versionName = "1.0.0" @@ -472,4 +473,8 @@ dependencies { testImplementation(libs.ktor.client.mock) testImplementation(libs.multiplatform.settings) testImplementation(libs.multiplatform.settings.test) + androidTestImplementation(libs.androidx.test.junit) + androidTestImplementation(libs.androidx.test.espresso) + androidTestImplementation(libs.androidx.security.crypto) + androidTestImplementation(libs.multiplatform.settings) } diff --git a/androidApp/src/androidTest/kotlin/com/devil/phoenixproject/data/local/AndroidDatabaseFileMigrationTest.kt b/androidApp/src/androidTest/kotlin/com/devil/phoenixproject/data/local/AndroidDatabaseFileMigrationTest.kt new file mode 100644 index 000000000..511cbda1d --- /dev/null +++ b/androidApp/src/androidTest/kotlin/com/devil/phoenixproject/data/local/AndroidDatabaseFileMigrationTest.kt @@ -0,0 +1,206 @@ +package com.devil.phoenixproject.data.local + +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import app.cash.sqldelight.db.QueryResult +import app.cash.sqldelight.db.SqlDriver +import com.devil.phoenixproject.database.PhoenixDatabase +import java.io.File +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class AndroidDatabaseFileMigrationTest { + private lateinit var context: Context + + @Before + fun setUp() { + context = InstrumentationRegistry.getInstrumentation().targetContext + deleteAllMigrationArtifacts() + } + + @After + fun tearDown() { + deleteAllMigrationArtifacts() + } + + @Test + fun populatedLegacyDatabaseMigratesAndRecoveryIsRemovedOnSecondLaunch() { + createLegacyDatabase(value = "kept-row").close() + + val firstDriver = DriverFactory(context).createDriver() + assertEquals("kept-row", firstDriver.queryString("SELECT value FROM MigrationProbe")) + firstDriver.close() + + assertFalse(databaseFile(LEGACY_DATABASE).exists()) + assertTrue(databaseFile(TARGET_DATABASE).exists()) + assertTrue(databaseFile(RECOVERY_DATABASE).exists()) + assertNoLegacySidecars() + + // Recovery validation may create transient SQLite sidecars. Cleanup owns + // the entire recovery artifact, not only its main database file. + databaseFile("$RECOVERY_DATABASE-wal").writeBytes(byteArrayOf()) + databaseFile("$RECOVERY_DATABASE-shm").writeBytes(byteArrayOf()) + + val secondDriver = DriverFactory(context).createDriver() + assertEquals("kept-row", secondDriver.queryString("SELECT value FROM MigrationProbe")) + secondDriver.close() + + assertNoArtifact(RECOVERY_DATABASE) + assertNoLegacyArtifacts() + } + + @Test + fun committedWalRowsAreCheckpointedBeforeLegacyCutover() { + val legacy = createLegacyDatabase(value = "main-row") + legacy.enableWriteAheadLogging() + legacy.rawQuery("PRAGMA wal_autocheckpoint = 0", null).use { cursor -> + check(cursor.moveToFirst()) { "wal_autocheckpoint pragma returned no row" } + } + legacy.execSQL("INSERT INTO MigrationProbe(value) VALUES ('wal-row')") + assertTrue("test precondition: WAL should exist", databaseFile("$LEGACY_DATABASE-wal").exists()) + + val driver = DriverFactory(context).createDriver() + assertEquals(2L, driver.queryLong("SELECT COUNT(*) FROM MigrationProbe")) + assertEquals("wal-row", driver.queryString("SELECT value FROM MigrationProbe ORDER BY rowid DESC LIMIT 1")) + driver.close() + legacy.close() + + assertNoLegacyArtifacts() + assertTrue(databaseFile(TARGET_DATABASE).exists()) + assertTrue(databaseFile(RECOVERY_DATABASE).exists()) + } + + @Test + fun corruptLegacyDatabaseBlocksWithoutCreatingPhoenixTarget() { + databaseFile(LEGACY_DATABASE).apply { + parentFile?.mkdirs() + writeBytes("not a sqlite database".encodeToByteArray()) + } + + val failure = runCatching { + val driver = DriverFactory(context).createDriver() + try { + driver.queryLong("PRAGMA user_version") + } finally { + driver.close() + } + }.exceptionOrNull() + + assertTrue(failure is IllegalStateException) + assertTrue(databaseFile(LEGACY_DATABASE).exists()) + assertFalse(databaseFile(TARGET_DATABASE).exists()) + assertFalse(databaseFile(RECOVERY_DATABASE).exists()) + } + + @Test + fun interruptedStagingIsDiscardedAndMigrationRestartsFromLegacy() { + createLegacyDatabase(value = "canonical-row").close() + databaseFile(STAGING_DATABASE).writeBytes("incomplete".encodeToByteArray()) + + val driver = DriverFactory(context).createDriver() + assertEquals("canonical-row", driver.queryString("SELECT value FROM MigrationProbe")) + driver.close() + + assertFalse(databaseFile(STAGING_DATABASE).exists()) + assertFalse(databaseFile(LEGACY_DATABASE).exists()) + assertTrue(databaseFile(TARGET_DATABASE).exists()) + assertTrue(databaseFile(RECOVERY_DATABASE).exists()) + } + + @Test + fun freshInstallCreatesOnlyPhoenixDatabaseArtifacts() { + val driver = DriverFactory(context).createDriver() + assertEquals(PhoenixDatabase.Schema.version, driver.queryLong("PRAGMA user_version")) + driver.close() + + assertTrue(databaseFile(TARGET_DATABASE).exists()) + assertFalse(databaseFile(RECOVERY_DATABASE).exists()) + assertNoLegacyArtifacts() + } + + private fun createLegacyDatabase(value: String): SQLiteDatabase { + val file = databaseFile(LEGACY_DATABASE) + file.parentFile?.mkdirs() + return SQLiteDatabase.openOrCreateDatabase(file, null).apply { + execSQL("CREATE TABLE MigrationProbe(value TEXT NOT NULL)") + execSQL("INSERT INTO MigrationProbe(value) VALUES (?)", arrayOf(value)) + execSQL("PRAGMA user_version = ${PhoenixDatabase.Schema.version}") + } + } + + private fun SqlDriver.queryLong(sql: String): Long { + var result: Long? = null + executeQuery( + identifier = null, + sql = sql, + mapper = { cursor -> + check(cursor.next().value) { "Expected one row for $sql" } + result = cursor.getLong(0) + QueryResult.Value(Unit) + }, + parameters = 0, + ) + return checkNotNull(result) + } + + private fun SqlDriver.queryString(sql: String): String { + var result: String? = null + executeQuery( + identifier = null, + sql = sql, + mapper = { cursor -> + check(cursor.next().value) { "Expected one row for $sql" } + result = cursor.getString(0) + QueryResult.Value(Unit) + }, + parameters = 0, + ) + return checkNotNull(result) + } + + private fun assertNoLegacyArtifacts() { + assertFalse(databaseFile(LEGACY_DATABASE).exists()) + assertNoLegacySidecars() + } + + private fun assertNoLegacySidecars() { + LEGACY_SIDECAR_SUFFIXES.forEach { suffix -> + assertFalse("legacy sidecar remains: $suffix", databaseFile("$LEGACY_DATABASE$suffix").exists()) + } + } + + private fun assertNoArtifact(name: String) { + assertFalse(databaseFile(name).exists()) + LEGACY_SIDECAR_SUFFIXES.forEach { suffix -> + assertFalse("artifact sidecar remains: $name$suffix", databaseFile("$name$suffix").exists()) + } + } + + private fun deleteAllMigrationArtifacts() { + listOf(LEGACY_DATABASE, TARGET_DATABASE, RECOVERY_DATABASE, STAGING_DATABASE, LOCK_FILE).forEach { name -> + databaseFile(name).delete() + } + listOf(LEGACY_DATABASE, TARGET_DATABASE, RECOVERY_DATABASE, STAGING_DATABASE).forEach { name -> + LEGACY_SIDECAR_SUFFIXES.forEach { suffix -> databaseFile("$name$suffix").delete() } + } + } + + private fun databaseFile(name: String): File = context.getDatabasePath(name) + + private companion object { + const val LEGACY_DATABASE = "vitruvian.db" + const val TARGET_DATABASE = "phoenix.db" + const val STAGING_DATABASE = "phoenix.db.migrating" + const val RECOVERY_DATABASE = "phoenix-recovery.db" + const val LOCK_FILE = "phoenix-db-migration.lock" + val LEGACY_SIDECAR_SUFFIXES = listOf("-wal", "-shm", "-journal") + } +} diff --git a/androidApp/src/androidTest/kotlin/com/devil/phoenixproject/data/preferences/AndroidPreferenceFileMigrationTest.kt b/androidApp/src/androidTest/kotlin/com/devil/phoenixproject/data/preferences/AndroidPreferenceFileMigrationTest.kt new file mode 100644 index 000000000..6174e216b --- /dev/null +++ b/androidApp/src/androidTest/kotlin/com/devil/phoenixproject/data/preferences/AndroidPreferenceFileMigrationTest.kt @@ -0,0 +1,138 @@ +package com.devil.phoenixproject.data.preferences + +import android.content.Context +import android.content.SharedPreferences +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKeys +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.devil.phoenixproject.di.SecureSettingsQualifier +import com.devil.phoenixproject.di.platformModule +import com.russhwolf.settings.Settings +import java.io.File +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.koin.android.ext.koin.androidContext +import org.koin.core.context.startKoin +import org.koin.core.context.stopKoin + +@RunWith(AndroidJUnit4::class) +class AndroidPreferenceFileMigrationTest { + private lateinit var context: Context + + @Before + fun setUp() { + stopKoin() + context = InstrumentationRegistry.getInstrumentation().targetContext + deleteAllPreferenceArtifacts() + } + + @After + fun tearDown() { + stopKoin() + deleteAllPreferenceArtifacts() + } + + @Test + fun plaintextAndEncryptedFilesMigrateAndCleanupAfterRestart() { + context.getSharedPreferences(LEGACY_PLAINTEXT, Context.MODE_PRIVATE).edit() + .putString("string", "value") + .putBoolean("boolean", true) + .putInt("int", 7) + .putLong("long", 9L) + .putFloat("float", 1.5f) + .putStringSet("strings", setOf("one", "two")) + .commit() + encrypted(LEGACY_ENCRYPTED).edit() + .putString("token", "secret") + .commit() + preferenceBackupFile(LEGACY_PLAINTEXT).writeBytes(byteArrayOf()) + preferenceBackupFile(LEGACY_ENCRYPTED).writeBytes(byteArrayOf()) + + val firstLaunch = startPreferenceKoin() + firstLaunch.get() + firstLaunch.get(SecureSettingsQualifier) + + val plaintextTarget = context.getSharedPreferences(TARGET_PLAINTEXT, Context.MODE_PRIVATE) + val encryptedTarget = encrypted(TARGET_ENCRYPTED) + assertEquals("value", plaintextTarget.getString("string", null)) + assertTrue(plaintextTarget.getBoolean("boolean", false)) + assertEquals(7, plaintextTarget.getInt("int", 0)) + assertEquals(9L, plaintextTarget.getLong("long", 0L)) + assertEquals(1.5f, plaintextTarget.getFloat("float", 0f)) + assertEquals(setOf("one", "two"), plaintextTarget.getStringSet("strings", emptySet())) + assertEquals("secret", encryptedTarget.getString("token", null)) + assertNoFileOrBackup(LEGACY_PLAINTEXT) + assertNoFileOrBackup(LEGACY_ENCRYPTED) + assertTrue(preferenceFile(RECOVERY_PLAINTEXT).exists()) + assertTrue(preferenceFile(RECOVERY_ENCRYPTED).exists()) + + stopKoin() + val secondLaunch = startPreferenceKoin() + secondLaunch.get() + secondLaunch.get(SecureSettingsQualifier) + + assertNoFileOrBackup(RECOVERY_PLAINTEXT) + assertNoFileOrBackup(RECOVERY_ENCRYPTED) + assertEquals("value", context.getSharedPreferences(TARGET_PLAINTEXT, Context.MODE_PRIVATE).getString("string", null)) + assertEquals("secret", encrypted(TARGET_ENCRYPTED).getString("token", null)) + } + + private fun startPreferenceKoin() = startKoin { + androidContext(context) + modules(platformModule) + }.koin + + private fun encrypted(name: String): SharedPreferences { + val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC) + return EncryptedSharedPreferences.create( + name, + masterKeyAlias, + context, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, + ) + } + + private fun assertNoFileOrBackup(name: String) { + assertFalse("preference XML remains: $name", preferenceFile(name).exists()) + assertFalse("preference backup remains: $name", preferenceBackupFile(name).exists()) + } + + private fun deleteAllPreferenceArtifacts() { + ALL_FILES.forEach { name -> + context.deleteSharedPreferences(name) + preferenceFile(name).delete() + preferenceBackupFile(name).delete() + } + } + + private fun preferenceFile(name: String): File = File( + File(context.applicationInfo.dataDir, "shared_prefs"), + "$name.xml", + ) + + private fun preferenceBackupFile(name: String): File = File("${preferenceFile(name).path}.bak") + + private companion object { + const val LEGACY_PLAINTEXT = "vitruvian_preferences" + const val TARGET_PLAINTEXT = "phoenix_preferences" + const val RECOVERY_PLAINTEXT = "phoenix_preferences_recovery" + const val LEGACY_ENCRYPTED = "vitruvian_secure_preferences" + const val TARGET_ENCRYPTED = "phoenix_secure_preferences" + const val RECOVERY_ENCRYPTED = "phoenix_secure_preferences_recovery" + val ALL_FILES = listOf( + LEGACY_PLAINTEXT, + TARGET_PLAINTEXT, + RECOVERY_PLAINTEXT, + LEGACY_ENCRYPTED, + TARGET_ENCRYPTED, + RECOVERY_ENCRYPTED, + ) + } +} diff --git a/androidApp/src/debug/kotlin/com/devil/phoenixproject/qa/ProfileQaDebugApp.kt b/androidApp/src/debug/kotlin/com/devil/phoenixproject/qa/ProfileQaDebugApp.kt index 9825b7c13..d2712c6d5 100644 --- a/androidApp/src/debug/kotlin/com/devil/phoenixproject/qa/ProfileQaDebugApp.kt +++ b/androidApp/src/debug/kotlin/com/devil/phoenixproject/qa/ProfileQaDebugApp.kt @@ -1,6 +1,6 @@ package com.devil.phoenixproject.qa -import com.devil.phoenixproject.VitruvianApp +import com.devil.phoenixproject.PhoenixApp import com.devil.phoenixproject.data.repository.AssessmentRepository import com.devil.phoenixproject.data.repository.ExerciseRepository import com.devil.phoenixproject.data.repository.PersonalRecordRepository @@ -11,11 +11,11 @@ import com.devil.phoenixproject.data.repository.WorkoutRepository import com.devil.phoenixproject.data.sync.PortalApiClient import com.devil.phoenixproject.data.sync.PortalTokenStorage import com.devil.phoenixproject.data.sync.SupabaseConfig -import com.devil.phoenixproject.database.VitruvianDatabase +import com.devil.phoenixproject.database.PhoenixDatabase import org.koin.core.context.loadKoinModules import org.koin.dsl.module -class ProfileQaDebugApp : VitruvianApp() { +class ProfileQaDebugApp : PhoenixApp() { override fun onCreate() { super.onCreate() @@ -31,7 +31,7 @@ class ProfileQaDebugApp : VitruvianApp() { personalRecordRepository = get(), assessmentRepository = get(), velocityOneRepMaxRepository = get(), - database = get(), + database = get(), ) } single { diff --git a/androidApp/src/debug/kotlin/com/devil/phoenixproject/qa/ProfileQaSeeder.kt b/androidApp/src/debug/kotlin/com/devil/phoenixproject/qa/ProfileQaSeeder.kt index 5b522b5d0..a0231eced 100644 --- a/androidApp/src/debug/kotlin/com/devil/phoenixproject/qa/ProfileQaSeeder.kt +++ b/androidApp/src/debug/kotlin/com/devil/phoenixproject/qa/ProfileQaSeeder.kt @@ -8,7 +8,7 @@ import com.devil.phoenixproject.data.repository.UserProfile import com.devil.phoenixproject.data.repository.UserProfileRepository import com.devil.phoenixproject.data.repository.VelocityOneRepMaxRepository import com.devil.phoenixproject.data.repository.WorkoutRepository -import com.devil.phoenixproject.database.VitruvianDatabase +import com.devil.phoenixproject.database.PhoenixDatabase import com.devil.phoenixproject.domain.model.CoreProfilePreferences import com.devil.phoenixproject.domain.model.JustLiftDefaultsDocument import com.devil.phoenixproject.domain.model.LedPreferences @@ -45,9 +45,9 @@ interface ProfileQaFixtureRowCleanup { } private class DatabaseProfileQaFixtureRowCleanup( - database: VitruvianDatabase, + database: PhoenixDatabase, ) : ProfileQaFixtureRowCleanup { - private val queries = database.vitruvianDatabaseQueries + private val queries = database.phoenixDatabaseQueries override fun deletePersonalRecord(id: Long) { queries.deletePersonalRecordById(id) @@ -66,7 +66,7 @@ class ProfileQaSeeder( private val personalRecordRepository: PersonalRecordRepository, private val assessmentRepository: AssessmentRepository, private val velocityOneRepMaxRepository: VelocityOneRepMaxRepository, - database: VitruvianDatabase, + database: PhoenixDatabase, private val fixtureRowCleanup: ProfileQaFixtureRowCleanup = DatabaseProfileQaFixtureRowCleanup(database), ) { diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml index 499497a83..e01aba1d4 100644 --- a/androidApp/src/main/AndroidManifest.xml +++ b/androidApp/src/main/AndroidManifest.xml @@ -83,7 +83,7 @@ + android:theme="@style/Theme.PhoenixProject"> diff --git a/androidApp/src/main/kotlin/com/devil/phoenixproject/MainActivity.kt b/androidApp/src/main/kotlin/com/devil/phoenixproject/MainActivity.kt index e61bcfdfa..6bc62613f 100644 --- a/androidApp/src/main/kotlin/com/devil/phoenixproject/MainActivity.kt +++ b/androidApp/src/main/kotlin/com/devil/phoenixproject/MainActivity.kt @@ -11,7 +11,6 @@ import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import co.touchlab.kermit.Logger -import com.devil.phoenixproject.presentation.components.RequireBlePermissions import com.devil.phoenixproject.presentation.viewmodel.ThemeViewModel import com.devil.phoenixproject.ui.theme.NightSample import com.devil.phoenixproject.ui.theme.ThemeMode @@ -37,11 +36,7 @@ class MainActivity : ComponentActivity() { navigationBarStyle = systemBarStyle, ) setContent { - // Require BLE permissions before showing the app - // Permission screens have their own theme, App provides its own theme - RequireBlePermissions { - AndroidAppHost() - } + AndroidAppHost() } } diff --git a/androidApp/src/main/kotlin/com/devil/phoenixproject/VitruvianApp.kt b/androidApp/src/main/kotlin/com/devil/phoenixproject/PhoenixApp.kt similarity index 88% rename from androidApp/src/main/kotlin/com/devil/phoenixproject/VitruvianApp.kt rename to androidApp/src/main/kotlin/com/devil/phoenixproject/PhoenixApp.kt index 97c4da43b..9064da67b 100644 --- a/androidApp/src/main/kotlin/com/devil/phoenixproject/VitruvianApp.kt +++ b/androidApp/src/main/kotlin/com/devil/phoenixproject/PhoenixApp.kt @@ -11,23 +11,19 @@ import coil3.SingletonImageLoader import coil3.network.ktor3.KtorNetworkFetcherFactory import coil3.request.crossfade import coil3.util.DebugLogger -import com.devil.phoenixproject.data.migration.MigrationManager import com.devil.phoenixproject.data.sync.SupabaseConfig import com.devil.phoenixproject.di.initKoin import com.devil.phoenixproject.ui.theme.applyPersistedApplicationNightMode import com.devil.phoenixproject.util.ActivityHolder import com.devil.phoenixproject.util.DeviceInfo -import org.koin.android.ext.android.inject import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger import org.koin.dsl.module -open class VitruvianApp : +open class PhoenixApp : Application(), SingletonImageLoader.Factory { - private val migrationManager: MigrationManager by inject() - override fun attachBaseContext(base: Context) { super.attachBaseContext(base) applyPersistedApplicationNightMode(this) @@ -49,7 +45,7 @@ open class VitruvianApp : initKoin { androidLogger() - androidContext(this@VitruvianApp) + androidContext(this@PhoenixApp) modules( module { single { @@ -62,9 +58,6 @@ open class VitruvianApp : ) } - // Start the required profile preference migration gate after Koin is initialized. - migrationManager.checkAndRunMigrations() - // H11: Register ActivityHolder via lifecycle callbacks instead of manual // calls in each Activity. Ensures the reference is always current across // config changes and multi-activity scenarios. @@ -87,7 +80,7 @@ open class VitruvianApp : override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} }) - Logger.d(tag = "VitruvianApp") { "Application initialized" } + Logger.d(tag = "PhoenixApp") { "Application initialized" } } override fun newImageLoader(context: coil3.PlatformContext): ImageLoader = ImageLoader.Builder(context) diff --git a/androidApp/src/main/kotlin/com/devil/phoenixproject/service/WorkoutForegroundService.kt b/androidApp/src/main/kotlin/com/devil/phoenixproject/service/WorkoutForegroundService.kt index 61683ac02..76a51e0eb 100644 --- a/androidApp/src/main/kotlin/com/devil/phoenixproject/service/WorkoutForegroundService.kt +++ b/androidApp/src/main/kotlin/com/devil/phoenixproject/service/WorkoutForegroundService.kt @@ -23,7 +23,7 @@ import com.devil.phoenixproject.presentation.manager.WorkoutServiceProtocol class WorkoutForegroundService : Service() { companion object { - const val CHANNEL_ID = "vitruvian_workout_channel" + const val CHANNEL_ID = "phoenix_workout_channel" const val NOTIFICATION_ID = 1 private val log = Logger.withTag("WorkoutForegroundService") diff --git a/androidApp/src/main/kotlin/com/devil/phoenixproject/ui/theme/AndroidTheme.kt b/androidApp/src/main/kotlin/com/devil/phoenixproject/ui/theme/AndroidTheme.kt index 3d7267a50..ccb803735 100644 --- a/androidApp/src/main/kotlin/com/devil/phoenixproject/ui/theme/AndroidTheme.kt +++ b/androidApp/src/main/kotlin/com/devil/phoenixproject/ui/theme/AndroidTheme.kt @@ -7,7 +7,7 @@ import androidx.compose.runtime.SideEffect import androidx.compose.ui.platform.LocalView import androidx.core.view.WindowCompat import com.devil.phoenixproject.ui.theme.ThemeMode as SharedThemeMode -import com.devil.phoenixproject.ui.theme.VitruvianTheme as SharedVitruvianTheme +import com.devil.phoenixproject.ui.theme.PhoenixTheme as SharedPhoenixTheme /** * Android-specific theme wrapper. @@ -15,17 +15,17 @@ import com.devil.phoenixproject.ui.theme.VitruvianTheme as SharedVitruvianTheme * Note: enableEdgeToEdge() in MainActivity handles status bar coloring. */ @Deprecated( - message = "Use the shared VitruvianTheme(themeMode = ..., dynamicColorEnabled = ...) overload so ThemeMode.SYSTEM is preserved.", + message = "Use the shared PhoenixTheme(themeMode = ..., dynamicColorEnabled = ...) overload so ThemeMode.SYSTEM is preserved.", ) @Composable -fun VitruvianTheme( +fun PhoenixTheme( darkTheme: Boolean = isSystemInDarkTheme(), dynamicColor: Boolean = false, content: @Composable () -> Unit, ) { val themeMode = if (darkTheme) SharedThemeMode.DARK else SharedThemeMode.LIGHT - SharedVitruvianTheme(themeMode = themeMode, dynamicColorEnabled = dynamicColor) { + SharedPhoenixTheme(themeMode = themeMode, dynamicColorEnabled = dynamicColor) { val view = LocalView.current if (!view.isInEditMode) { diff --git a/androidApp/src/main/res/drawable-xxxhdpi/phoenix_logo_foreground.png b/androidApp/src/main/res/drawable-xxxhdpi/phoenix_logo_foreground.png new file mode 100644 index 000000000..d20f024ef Binary files /dev/null and b/androidApp/src/main/res/drawable-xxxhdpi/phoenix_logo_foreground.png differ diff --git a/androidApp/src/main/res/drawable-xxxhdpi/vitphoe_logo_foreground.png b/androidApp/src/main/res/drawable-xxxhdpi/vitphoe_logo_foreground.png deleted file mode 100644 index eb7c76526..000000000 Binary files a/androidApp/src/main/res/drawable-xxxhdpi/vitphoe_logo_foreground.png and /dev/null differ diff --git a/androidApp/src/main/res/drawable/ic_launcher_foreground.xml b/androidApp/src/main/res/drawable/ic_launcher_foreground.xml index d0f2c9315..d94c6f016 100644 --- a/androidApp/src/main/res/drawable/ic_launcher_foreground.xml +++ b/androidApp/src/main/res/drawable/ic_launcher_foreground.xml @@ -8,6 +8,6 @@ diff --git a/androidApp/src/main/res/drawable/ic_launcher_monochrome.xml b/androidApp/src/main/res/drawable/ic_launcher_monochrome.xml index 2a6036fee..234da0e1c 100644 --- a/androidApp/src/main/res/drawable/ic_launcher_monochrome.xml +++ b/androidApp/src/main/res/drawable/ic_launcher_monochrome.xml @@ -6,6 +6,6 @@ android:insetBottom="20dp"> diff --git a/androidApp/src/main/res/values-night/themes.xml b/androidApp/src/main/res/values-night/themes.xml index 1f24452aa..8a46398d9 100644 --- a/androidApp/src/main/res/values-night/themes.xml +++ b/androidApp/src/main/res/values-night/themes.xml @@ -1,6 +1,6 @@ -