Add toggleable stall detection feature - #7
Merged
Conversation
Add stallDetectionEnabled flag to allow users to toggle stall detection auto-stop behavior on Just Lift and AMRAP workout configurations: - Add stallDetectionEnabled to WorkoutParameters (default: true) - Add stallDetectionEnabled to RoutineExercise model for per-exercise config - Update checkAutoStop logic to check stallDetectionEnabled flag - Add stall detection toggle card to JustLiftScreen - Add stall detection toggle to ExerciseEditBottomSheet (shows for AMRAP) - Update ExerciseConfigViewModel to handle stallDetectionEnabled state - Update routine workflow to pass stallDetectionEnabled to WorkoutParameters - Update JustLiftDefaults to persist stallDetectionEnabled setting Stall detection auto-stops a set when movement pauses for 5 seconds. Users can now choose whether they want this behavior enabled per mode.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds a toggleable stall detection feature for Just Lift and AMRAP workout modes. The stall detection auto-stops a set when movement pauses for 5 seconds, and users can now control whether this behavior is enabled.
Key changes:
- Added
stallDetectionEnabledboolean flag (default:true) to WorkoutParameters, RoutineExercise, and JustLiftDefaults data classes - Updated auto-stop logic to check both mode compatibility AND the stallDetectionEnabled flag
- Added stall detection toggle UI controls in JustLiftScreen and ExerciseEditBottomSheet
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| Models.kt | Added stallDetectionEnabled field to WorkoutParameters with default value true |
| Routine.kt | Added stallDetectionEnabled field to RoutineExercise model for per-exercise configuration |
| MainViewModel.kt | Updated stall detection logic to respect the toggleable flag; added stallDetectionEnabled to JustLiftDefaults; updated routine workflow to pass stallDetectionEnabled through WorkoutParameters |
| ExerciseConfigViewModel.kt | Added state management for stallDetectionEnabled with getter/setter methods |
| JustLiftScreen.kt | Added stall detection toggle card UI and state handling; included setting in LaunchedEffect dependencies |
| ExerciseEditBottomSheet.kt | Added stall detection toggle UI that displays when exercise has AMRAP sets |
| PreferencesManager.kt | Added stallDetectionEnabled field to JustLiftDefaults data class for persistence support |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -224,7 +224,8 @@ data class WorkoutParameters( | |||
| val selectedExerciseId: String? = null, | |||
| val isAMRAP: Boolean = false, // AMRAP (As Many Reps As Possible) - disables auto-stop | |||
There was a problem hiding this comment.
The comment "AMRAP (As Many Reps As Possible) - disables auto-stop" is now outdated since AMRAP mode can have stall detection enabled when stallDetectionEnabled is true. The comment should be updated to reflect that stall detection is now configurable.
Suggested change
| val isAMRAP: Boolean = false, // AMRAP (As Many Reps As Possible) - disables auto-stop | |
| val isAMRAP: Boolean = false, // AMRAP (As Many Reps As Possible) - auto-stop (stall detection) is configurable via stallDetectionEnabled |
9thLevelSoftware
added a commit
that referenced
this pull request
Jan 5, 2026
Migration 6 was already used on main branch for Training Cycle tables. Sync tracking fields migration needs to be #7 to avoid conflict. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
9thLevelSoftware
added a commit
that referenced
this pull request
Feb 16, 2026
… issue **Issues Fixed:** - #2: Race condition - SingleExerciseScreen now awaits routine load via loadRoutineAsync() before ensureConnection(), preventing PR weight fetch from racing with onConnected callback - #4: Keypad input - CompactNumberPicker.ios.kt sanitizes input and handles parse failure by keeping current value instead of max (242 lbs) - #6: Unilateral weight - ActiveSessionEngine detects single-cable exercises and uses maxOf(loadA, loadB) instead of totalLoad/2 - #7: Mode persistence - SqlDelightWorkoutRepository.parseProgramMode() handles unprefixed legacy mode strings ("Pump" vs "Program:Pump") **Diagnostic Logging for Issue #5:** Added WEIGHT_DEBUG logging at 5 points to trace weight value flow: - iOS CompactNumberPicker scroll handler - JustLiftScreen picker onValueChange - JustLiftScreen params update - ActiveSessionEngine set completion - SetSummaryCard display calculation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
9thLevelSoftware
added a commit
that referenced
this pull request
Apr 19, 2026
…lback Resolves audit item #7 (.planning/audit/06-dto-drift-matrix.md). Server-side mobile-sync-pull now enforces HTTP 413 when any parity list exceeds 500 entries (replacing the prior silent-empty behavior). This commit adds matching client-side capping so legitimate power-user accounts do not get rejected. - `SyncConfig.MAX_PARITY_IDS = 500` new constant matching the server cap. - `capParity()` helper in `runPullLoop` truncates each parity list to the last MAX_PARITY_IDS entries when oversized; the pull then falls back to server-side lastSync delta semantics for the older tail, and local dedupe handles overlap. Logs a warning at WARN level when truncation occurs so power users' accounts are observable in telemetry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8 tasks
9thLevelSoftware
added a commit
that referenced
this pull request
Apr 19, 2026
Test was authored with the pre-Phase-4.1 behavior (mobile sends uncapped knownEntityIds; server silently returns empty for >500). Phase 4.1 (audit item #7) inverted that contract: server now enforces HTTP 413 at 500, mobile capParity() truncates to the most recent 500 entries via takeLast(). Test renamed + assertion flipped to match new contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9thLevelSoftware
added a commit
that referenced
this pull request
Apr 19, 2026
…370) * cursor: iOS CI, Android FGS, BLE/sync, and Koin iOS @throws Made-with: Cursor * fix(ios,ci,android): repair Koin iOS symbol, Supabase xcconfig, and release guard - iOS: Swift was calling `KoinInitKt.doInitKoin()`, but the new `@Throws` entrypoint lives in `shared/iosMain/.../KoinInitIos.kt`, so Kotlin/Native exports it as `KoinInitIosKt.doInitKoin()`. Without this fix the iOS app fails to compile. Switched the Swift call (with `try`) and updated the KoinInit.kt doc comment + iosApp/README accordingly. - CI: The three iOS workflows wrote `Supabase.xcconfig` without escaping `//`, which xcconfig treats as an inline comment. A secret like `https://xxx.supabase.co` was being truncated to `https:` at build time, breaking Supabase in TestFlight/Release. Apply the same `/$()/` escape already used by the committed `.example` template via `sed 's|//|/$()/|g'` to both URL and anon-key fields. - Android: Replaced `findByName("assembleRelease").forEach { doFirst }` with `tasks.matching { ... }.configureEach { doFirst }` so the explicit-versionCode guard attaches regardless of when AGP registers the release tasks (findByName at configuration time silently no-ops if the task isn't registered yet). Made-with: Cursor * fix(mobile): beta audit hardening + DTO drift Phase 1 Bundles the in-progress beta audit remediation work with the first phase of the DTO drift remediation plan (.planning/audit/06-dto-drift-matrix.md, resolved items #2 wire-contract portion, #3, #4). Beta audit hardening: - .gitignore (C1): cover Supabase.xcconfig in any VitruvianPhoenix subdir so a stray sibling file cannot leak the anon key - PortalAuthRepository (C6): explicit failure messages for social sign-in stubs; consumers can surface a clear reason instead of string-sniffing - GamificationViewModel (H): surface load errors via StateFlow instead of swallowing silently; log via kermit - RepCounterFromMachine (Issue #163): remove repCountersNeedBaseline baseline suppression; first post-reset rep now counts immediately, matching expected UX after BLE reconnect - SafeWordListener (android/ios): hardening of listener lifecycle - FakePortalApiClient: test double alignment - New common-test suites covering asymmetry threshold, error classification, portal mapping weight, pull pagination, push limits, token refresh, sync backoff, sync failure cap, velocity zone boundaries, workout phase round trip - Supabase.xcconfig + xcodeproj: secret wiring adjustment - androidApp/build.gradle.kts: build config cleanup DTO drift Phase 1: - PullWorkoutSessionDto (#2): add nullable `notes` field so session-level notes round-trip from portal. Mobile persistence is tracked separately (Phase 3.5) because WorkoutSession is per-exercise while portal sessions are per-workout; notes need a SessionNotes side-table keyed on routineSessionId rather than a straight column addition. - PortalSetDto (#3): documentation clarifying that prType/prPhase/prVolume are SEND-ONLY PR derivation hints consumed by the portal into personal_records; they are intentionally not persisted on the sets table. - PortalRepTelemetryDto (#4): update `cable` doc comment to reflect the canonical "A"|"B" wire format (A = left actuator, B = right actuator). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(mobile): extend ExternalActivityAckDto with localId/serverId/updatedAt Resolves audit items #5 and #10 (.planning/audit/06-dto-drift-matrix.md). - ExternalActivityAckDto now carries `localId`, `serverId`, and `updatedAt` (all nullable defaults so older response payloads still decode) so mobile can reconcile server-canonical metadata (notably the `updated_at` timestamp used to seed LWW convergence in Phase 3) onto local rows after push. - PortalSyncPushResponse.externalActivityIds marked @deprecated; prefer externalActivityKeys which now carries full ack metadata. Retained for one release for older adapters. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(mobile): cap parityIds at MAX_PARITY_IDS=500 with tail-window fallback Resolves audit item #7 (.planning/audit/06-dto-drift-matrix.md). Server-side mobile-sync-pull now enforces HTTP 413 when any parity list exceeds 500 entries (replacing the prior silent-empty behavior). This commit adds matching client-side capping so legitimate power-user accounts do not get rejected. - `SyncConfig.MAX_PARITY_IDS = 500` new constant matching the server cap. - `capParity()` helper in `runPullLoop` truncates each parity list to the last MAX_PARITY_IDS entries when oversized; the pull then falls back to server-side lastSync delta semantics for the older tail, and local dedupe handles overlap. Logs a warning at WARN level when truncation occurs so power users' accounts are observable in telemetry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(mobile): populate updatedAt on push DTOs + decode LWW rejections Phase 3.2 client-side companion to the portal LWW push handler swap. Adds optional `updatedAt: String?` (ISO 8601) to the three shared-edit push DTOs that will flow through LWW RPCs on the server: - PortalWorkoutSessionDto - PortalRoutineSyncDto - PortalTrainingCycleSyncDto PortalSyncAdapter populates each with `epochToIso8601(currentTimeMillis())` on every build so the server's LWW gate receives a real monotonic timestamp. When mobile domain models start tracking per-row updated_at end-to-end (Phase 3.3), swap the adapter to emit domain values instead. PortalSyncPushResponse now decodes a nullable-default `rejections: SyncRejectionsDto` field (with nested SyncRejectionDto entries), so the mobile SyncManager can log LWW rejections and trigger a repair pull. Older server responses without the field still decode because of the empty default. The server side uses NOW() when `updatedAt` is absent (pre-Phase-3.2 clients), so existing mobile builds continue to work unmodified while updated builds participate in LWW immediately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(mobile): self-cap + sliding-window client rate limit for sync Phase 4.2 of the DTO drift remediation plan. Resolves audit item #9. A misbehaving retry loop or a user with a huge local history should fail fast locally instead of wasting Edge Function invocations for payloads the server will reject with HTTP 413/429. SyncConfig additions (mirror server-side caps at mobile-sync-push): - MAX_SESSIONS_PER_BATCH = 10000 - MAX_ROUTINES_PER_BATCH = 10000 - MAX_CYCLES_PER_BATCH = 10000 (aligned with audit #6) - MAX_TELEMETRY_PER_BATCH = 10000 - MAX_PAYLOAD_BYTES = 9_500_000 (500 KiB below 10 MiB server cap) - PUSH_RATE_LIMIT_PER_MIN = 10 - PULL_RATE_LIMIT_PER_MIN = 20 - RATE_LIMIT_WINDOW_MS = 60_000 PortalApiClient.pushPortalPayload now validates the array caps and the serialized payload size BEFORE opening the HTTP connection. On violation it returns `Result.failure(PortalApiException(...))` with a clear message. The serialized bytes are reused (not re-encoded) for the request body so there is no double-JSON cost. New ClientRateLimiter is an in-memory sliding-window limiter keyed by operation ("push" / "pull"). Internal state is a Mutex-guarded deque of recent attempt timestamps. SyncManager's push/pull entry points call `tryAcquire()` at the very top and surface a 429-coded PortalApiException when denied. In-memory scope only for this phase — a persistent SQLDelight-backed variant is noted as a follow-up for when process restarts during a retry storm would defeat the window. SyncManager gains an optional `rateLimiter: ClientRateLimiter = ClientRateLimiter()` constructor parameter so existing DI wiring keeps working unchanged; tests can supply a `ClientRateLimiter(windowMillis = ...)` with a reset hook for deterministic assertions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(mobile): persist portal session notes via SessionNotes side-table Phase 3.5 of the DTO drift remediation plan. Closes the mobile persistence gap left from Phase 1.1 of audit item #2 (portal session-level notes round-trip on the wire but were not stored locally). The mobile WorkoutSession model is per-exercise (one portal session expands into N mobile rows keyed by routineSessionId), so adding a `notes` column to WorkoutSession would duplicate state across rows. Instead this phase introduces a single-row-per-portal-session side-table. Schema (migration 26.sqm + main VitruvianDatabase.sq): - CREATE TABLE SessionNotes( routineSessionId TEXT PRIMARY KEY, notes TEXT, updatedAt INTEGER ) - INDEX idx_session_notes_updated_at on (updatedAt) for delta queries. - upsertSessionNotesLww query gates writes on `excluded.updatedAt >= SessionNotes.updatedAt` (NULL stored treated as older). Uses the same LWW semantics shipping in Phase 3.1 RPCs server-side so portal edits from a newer device win-by-timestamp. - getSessionNotes / selectSessionNotesForIds for read paths. Repository surface: - SyncRepository gains `mergeSessionNotes(notes: Map<String, SessionNotesEntry>)` with a default no-op so existing test fakes keep compiling. - SqlDelightSyncRepository wraps the upsert in a single transaction. SyncManager pull merge: - Extracts non-blank session notes from the pull response into a Map keyed on `routineSessionId` (== portal session id). - Persists outside the main atomic merge so a notes-table failure is non-fatal — sessions remain consistent even if the side-table write fails. Logged at WARN. - updatedAt sourced from `PullWorkoutSessionDto.startedAt` parsed via kotlin.time.Instant. When the wire DTO eventually carries a real `updatedAt` field (Phase 3.3 mobile-side LWW), swap to that. Build: - SQLDelight schema version bumped 26 → 27 to match the new migration. UI surface (session detail screen rendering of notes) is intentionally out of scope for this commit and tracked as follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(mobile): SQLDelight 3.18-compatible SessionNotes upsert + manifest entry Two CI failures from PR #370: 1. SQLDelight gen failed: the dialect (sqlite_3_18) does not support INSERT ... ON CONFLICT DO UPDATE. Replaced the single `upsertSessionNotesLww` query with two SQLite-3.18-compatible queries (`selectSessionNotesUpdatedAt` + `upsertSessionNotes` using `INSERT OR REPLACE`) and moved the LWW gate into Kotlin (`SqlDelightSyncRepository.mergeSessionNotes`) where it runs inside the same transaction as the read. 2. iOS Schema Sync Check: SessionNotes missing from SchemaManifest.kt. Added the SchemaTableOperation entry plus a SchemaIndexOperation for `idx_session_notes_updated_at` so the iOS-side schema bootstrap stays parity-aligned with the SQLDelight schema. Also fixed `SELECT ... WHERE routineSessionId IN ?;` to the SQLDelight named-list form `WHERE routineSessionId IN :ids` (the bare `?` is not a valid bind for IN in SQLDelight). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(mobile): import currentTimeMillis in PortalSyncAdapter Phase 3.2 commit (3494794) added `currentTimeMillis()` calls for the new `updatedAt = epochToIso8601(currentTimeMillis())` LWW timestamps at lines 196 / 510 / 600 but missed the import. CI compile failed with 'Unresolved reference currentTimeMillis' on linux because the function lives in `com.devil.phoenixproject.util.KmpUtils`. Add the import. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(mobile): assert parity ids capped at MAX_PARITY_IDS, not uncapped Test was authored with the pre-Phase-4.1 behavior (mobile sends uncapped knownEntityIds; server silently returns empty for >500). Phase 4.1 (audit item #7) inverted that contract: server now enforces HTTP 413 at 500, mobile capParity() truncates to the most recent 500 entries via takeLast(). Test renamed + assertion flipped to match new contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9thLevelSoftware
added a commit
that referenced
this pull request
Jun 27, 2026
…asis (#517) Finding #7: generalize per-set weight helpers to resolve against the selected-basis baseline (baselineKgForCurrentBasis) instead of the max-weight PR. Rename resolvedSetWeightsKgFromCurrentPR -> resolvedSetWeightsKgFromCurrentBasis; per-set percent<->weight helpers and updateWeight now take/use baselineKg. onScalingBasisChange and the mode-independent baseline load re-sync visible set weights. Behavior is unchanged when basis == MAX_WEIGHT_PR. Finding #8: make the "Scale from" basis selector always available while % scaling is enabled (moved out of the baselineWeightKg != null guard), and gate the % toggle on hasAnyBaseline (any basis has data) rather than only the currently-selected basis — so a user with only an Est. 1RM can enable scaling, pick ESTIMATED_1RM, and proceed. Preview/slider/presets remain gated on the selected basis having a baseline. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E3r4QJcxvNCB1evniqwxJm
9thLevelSoftware
added a commit
that referenced
this pull request
Jun 27, 2026
…3-5) (#517) (#598) * fix(1rm): bump SQLDelight schema version to 38 for migrations 36/37 (#517) Version was stuck at 36; 36.sqm (36→37) and 37.sqm (37→38) would never run on upgrades. Updated version and comment to match SchemaParityTest's CURRENT_VERSION = 38L. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Z5vuLimeDX3GXvY1tGC6o * fix(1rm): reject NaN session MCV in personalized-MVT capture (#517) `NaN <= 0f` is false in IEEE 754, so a NaN value silently passed the old guard and could corrupt the rolling mean. Changed to the negated- positive form `!(x > 0f)` which correctly rejects NaN, 0, and negatives. Added a test asserting invoke(..., Float.NaN) returns false and makes no repo write. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Z5vuLimeDX3GXvY1tGC6o * fix(1rm): persist MCV on manual-stop sets so they feed velocity-1RM (#517) Manual-stop path in stopWorkout() built WorkoutSession without avgMcvMmS, so those sets were excluded from selectVelocityPointsByExercise (IS NOT NULL filter) and from personalized-MVT capture. Safety verified: biomechanicsEngine .reset() is not called anywhere in stopWorkout() before the session is built. Mirrors the bioSummary capture already present in saveWorkoutSession(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Z5vuLimeDX3GXvY1tGC6o * docs: add Phase 3 (% of 1RM scaling) implementation plan (#517) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Z5vuLimeDX3GXvY1tGC6o * feat(1rm): add ScalingBasis enum + RoutineExercise.scalingBasis (#517) * feat(1rm): persist RoutineExercise.scalingBasis, migration 38 (#517) - Add nullable scalingBasis TEXT column to RoutineExercise table - Create migration 38.sqm with ALTER TABLE ADD COLUMN - Add migration 38 to MigrationStatements.kt - Add SchemaHealOperation + SchemaTableOperation entry in SchemaManifest.kt - Bump SQLDelight schema version 38 → 39 in shared/build.gradle.kts - Bump CURRENT_VERSION 38L → 39L in SchemaParityTest - Update SqlDelightWorkoutRepository: mapper reads scalingBasis via ScalingBasis.valueOf(), insertRoutineExercise passes scalingBasis?.name - Add scalingBasis = null to all other insertRoutineExercise/Ignore call sites (SqlDelightSyncRepository, DataBackupManager, test helpers) - Add round-trip androidHostTest SqlDelightWorkoutRepositoryRoutineScalingTest Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Z5vuLimeDX3GXvY1tGC6o * fix(1rm): preserve scalingBasis across backup/restore (#517) Backup export/import dropped RoutineExercise.scalingBasis, so a user who set scalingBasis (e.g. ESTIMATED_1RM) silently lost it on restore. - BackupModels: add scalingBasis: String? = null to RoutineExerciseBackup (default null keeps pre-existing backup files loadable) - DataBackupManager.mapRoutineExerciseToBackup: capture exercise.scalingBasis (already the enum-name TEXT on the DB row) verbatim - DataBackupManager: both restore/import call sites now parse-and-validate the backup string via ScalingBasis.valueOf (invalid -> null) instead of hardcoding null - Add cross-database export->import round-trip test asserting ESTIMATED_1RM survives Sync-path nulls left as-is: scalingBasis stays mobile-local until the portal phase. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Z5vuLimeDX3GXvY1tGC6o * feat(1rm): resolve % of estimated-1RM in routine weights (#517) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Z5vuLimeDX3GXvY1tGC6o * test(1rm): make velocity-1RM fake id-aware + import cleanup (#517) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Z5vuLimeDX3GXvY1tGC6o * feat(1rm): add system-wide defaultScalingBasis preference (#517) Adds `UserPreferences.defaultScalingBasis: ScalingBasis = ScalingBasis.MAX_WEIGHT_PR` persisted via SettingsPreferencesManager (putString/getStringOrNull with runCatching fallback), exposed as `SettingsManager.defaultScalingBasis: StateFlow<ScalingBasis>` + `setDefaultScalingBasis(basis)`. FakePreferencesManager updated for tests. Round-trip test in SettingsManagerTest confirms RED→GREEN. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Z5vuLimeDX3GXvY1tGC6o * feat(1rm): UI for % of 1RM scaling basis + global default (#517) - ExerciseConfigViewModel: expose scalingBasis StateFlow + onScalingBasisChange setter; load from RoutineExercise.scalingBasis in initialize(); persist via onSave() copy. - ExerciseEditBottomSheet/WeightConfigurationCard: add 3-way SingleChoiceSegmentedButtonRow (Max-weight PR / Max-volume PR / Est. 1RM) shown when usePercentOfPR is on; bound to VM. - SettingsTab: add defaultScalingBasis param + 3-way segmented control near weight-programming settings; passes through NavGraph -> MainViewModel -> SettingsManager.setDefaultScalingBasis. - MainViewModel: add setDefaultScalingBasis delegation to SettingsManager. - ExerciseConfigViewModelTest: two new tests for scalingBasis round-trip and null back-compat. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Z5vuLimeDX3GXvY1tGC6o * fix(1rm): seed new routine exercises from default scaling basis (#517) New exercises added in the routine editor now adopt the system-wide defaultScalingBasis from UserPreferences, making the Settings control actually take effect. Editing an existing exercise is unaffected — the VM still loads the stored scalingBasis. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Z5vuLimeDX3GXvY1tGC6o * fix(1rm): fall back to PR for % of estimated-1RM when no estimate/stored 1RM (#517) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Z5vuLimeDX3GXvY1tGC6o * docs: add Phase 4 (velocity-1RM badges) implementation plan (#517) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Z5vuLimeDX3GXvY1tGC6o * feat(1rm): add tiered velocity-1RM improvement badges (#517) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Z5vuLimeDX3GXvY1tGC6o * feat(1rm): add getAllPassing velocity-1RM query (#517) Add selectAllPassingVelocityOneRepMaxByProfile SQL query and getAllPassing(profileId) method to VelocityOneRepMaxRepository interface and SqlDelightVelocityOneRepMaxRepository impl. Update Fake and two inline anonymous impls in androidHostTest to satisfy the new contract. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Z5vuLimeDX3GXvY1tGC6o * feat(1rm): count velocity-1RM improvements (#517) * feat(1rm): award velocity-1RM badges from post-save hook (#517) - Add GamificationManager.checkVelocityOneRepMaxBadges: filters VelocityOneRepMaxImprovements badges, awards unearned ones when improvementCount >= req.count, emits haptic + badgeEarnedEvents - Wire CountVelocityOneRepMaxImprovementsUseCase into DomainModule (single {}) and MainViewModel constructor - Extend onPostSaveComputed lambda: after computeVelocityOneRepMaxUseCase, count all passing estimates via countVelocityOneRepMaxImprovementsUseCase then call checkVelocityOneRepMaxBadges - Add explicit : GamificationManager type annotation to resolve Kotlin recursive type-checking error from the self-referential lambda - Update all 4 MainViewModel construction sites (PlatformModule.android, PlatformModule.ios, MainViewModelTest, WorkoutFlowE2ETest) - Add 2 GamificationManagerTest cases: bronze award at count=1, idempotent re-award guard Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Z5vuLimeDX3GXvY1tGC6o * docs(1rm): fix badge-stub comments + add velocity-badge log line (#517) * docs: add Phase 5 (velocity-1RM historical backfill) plan (#517) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Z5vuLimeDX3GXvY1tGC6o * feat(1rm): enumerate MCV-bearing exercises + hasEstimates (#517) Add two SQL queries (selectExerciseIdsWithVelocityData, countVelocityOneRepMaxByExercise), regen SQLDelight, and wire both into VelocityOneRepMaxRepository.hasEstimates + WorkoutRepository.getExerciseIdsWithVelocityData. Update FakeWorkoutRepository and FakeVelocityOneRepMaxRepository with sensible defaults; add hasEstimates override to the two inline anonymous VelocityOneRepMaxRepository stubs in WorkoutFlowE2ETest and MainViewModelTest. Extend SqlDelightVelocityOneRepMaxRepositoryTest and SqlDelightWorkoutRepositoryVelocityPointsTest with the new behaviour tests; 5+3 tests pass, 0 failures. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Z5vuLimeDX3GXvY1tGC6o * feat(1rm): parameterize compute window for backfill (#517) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Z5vuLimeDX3GXvY1tGC6o * feat(1rm): add BackfillVelocityOneRepMaxUseCase + run-once flag (#517) - BackfillVelocityOneRepMaxUseCase: function-typed collaborators pattern, iterates exercise IDs for a profile, skips those with existing estimates, calls computeAllTime for the rest, returns count of newly created estimates - velocityOneRepMaxBackfillDone: Boolean = false wired across UserPreferences, PreferencesManager (KEY + load + interface + impl), FakePreferencesManager, and SettingsManager (StateFlow + setter), mirroring gamificationEnabled exactly - commonTest: BackfillVelocityOneRepMaxUseCaseTest proves skip-if-hasEstimates logic and correct non-null count - androidHostTest: SettingsManagerTest round-trip for the new backfill-done flag Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Z5vuLimeDX3GXvY1tGC6o * feat(1rm): run velocity-1RM backfill once on startup (#517) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E3r4QJcxvNCB1evniqwxJm * fix(1rm): backfill the loaded active profile, not the init-time default (#517) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E3r4QJcxvNCB1evniqwxJm * fix(1rm): rethrow CancellationException in backfill trigger (#517) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Z5vuLimeDX3GXvY1tGC6o * fix(1rm): use explicit isNaN guard in personalized-MVT capture (#517) Replaces the non-idiomatic !(x > 0f) NaN filter (which a linter could "simplify" back to <= 0f, reintroducing the NaN bypass) with an explicit isNaN() || <= 0f check. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Z5vuLimeDX3GXvY1tGC6o * fix(1rm): persist full biomechanics + reset engine on manual-stop sets (#517) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E3r4QJcxvNCB1evniqwxJm * fix(1rm): preserve local scalingBasis across portal pulls (#517) Both delete+reinsert paths in mergePortalRoutines and the atomic merge path now capture localScalingBasisByExerciseId before the delete and re-apply it on insert, mirroring the existing rack-field preservation pattern. Adds a targeted test asserting ESTIMATED_1RM survives a portal pull that overwrites an existing routine. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E3r4QJcxvNCB1evniqwxJm * fix(1rm): backfill every profile before setting run-once flag (#517) The velocityOneRepMaxBackfillDone flag is global, so backfilling only the active profile permanently skipped every other profile's historical MCV data. Iterate all profiles (awaiting the loaded list) and backfill each before marking the run-once flag done. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Z5vuLimeDX3GXvY1tGC6o * fix(1rm): editor basis gating + preview from selected scaling basis (#517) - ExerciseConfigViewModel: add optional VelocityOneRepMaxRepository and ExerciseRepository constructor params; load max-volume PR alongside max-weight PR in loadPRForExercise (mode-dependent); load velocity estimate + stored Exercise.oneRepMaxKg once per initialize via new loadModeIndependentBaselines(); expose currentMaxVolumePR, velocityEstimateKg, storedOneRepMaxKg StateFlows; add baselineKgForCurrentBasis() mirroring ResolveRoutineWeightsUseCase's fallback chain; fix calculateResolvedWeight() to use it. - ExerciseEditBottomSheet: inject VelocityOneRepMaxRepository via koinInject() and pass with exerciseRepository to VM; collect new baseline state flows; compute baselineWeightKg from the effective scaling basis; add baselineWeightKg param to WeightConfigurationCard; fix switch gating (enabled = baselineWeightKg != null) so ESTIMATED_1RM and MAX_VOLUME_PR are accessible without a max-weight PR; fix preview to use baselineWeightKg instead of always max-weight PR; update label text per basis. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E3r4QJcxvNCB1evniqwxJm * docs: add Phase 6 (portal velocity-1RM display) design spec (#517) Cross-repo spec: sync velocity_estimated_1rm_kg to the portal and display it distinctly from the rep-based estimate. Display only; scaling-basis parity deferred to a separate spec. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Z5vuLimeDX3GXvY1tGC6o * docs: add Phase 6 (portal velocity-1RM display) cross-repo plan (#517) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Z5vuLimeDX3GXvY1tGC6o * fix(1rm): isolate per-profile backfill failures (#517) One profile's backfill failure no longer aborts the remaining profiles or leaves the run-once flag unset (which re-failed on the same profile every launch). Each profile is wrapped in its own try/catch; the flag is set after the loop regardless. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Z5vuLimeDX3GXvY1tGC6o * fix(1rm): per-set weights + basis selector use the selected scaling basis (#517) Finding #7: generalize per-set weight helpers to resolve against the selected-basis baseline (baselineKgForCurrentBasis) instead of the max-weight PR. Rename resolvedSetWeightsKgFromCurrentPR -> resolvedSetWeightsKgFromCurrentBasis; per-set percent<->weight helpers and updateWeight now take/use baselineKg. onScalingBasisChange and the mode-independent baseline load re-sync visible set weights. Behavior is unchanged when basis == MAX_WEIGHT_PR. Finding #8: make the "Scale from" basis selector always available while % scaling is enabled (moved out of the baselineWeightKg != null guard), and gate the % toggle on hasAnyBaseline (any basis has data) rather than only the currently-selected basis — so a user with only an Est. 1RM can enable scaling, pick ESTIMATED_1RM, and proceed. Preview/slider/presets remain gated on the selected basis having a baseline. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E3r4QJcxvNCB1evniqwxJm * fix(1rm): editor uses effectiveScalingBasis + re-syncs set weights after baseline load (#517) Finding A: the editor derived the current basis as scalingBasis ?: MAX_WEIGHT_PR, which is wrong for legacy rows where scalingBasis is null but prTypeForScaling = MAX_VOLUME. Add ExerciseConfigViewModel. effectiveScalingBasis() (= scalingBasis ?: fromPrType(prTypeForScaling)) and use it in baselineKgForCurrentBasis(); the screen now derives the same effective basis (collecting prTypeForScaling) and passes it to WeightConfigurationCard, which switches its scalingBasis param for effectiveScalingBasis. Legacy MAX_VOLUME rows now select MAX_VOLUME_PR and preview/per-set weights off the volume PR. Finding B: a MAX_VOLUME_PR exercise re-synced visible set weights after the max-weight PR loaded but before the volume PR was populated, leaving stale rows. Add resyncSetWeightsIfBaselineReady() (guarded: only when scaling on and the current-basis baseline is available) and call it after each async baseline load (volume PR, velocity estimate, stored 1RM). The sync only rewrites set weights from stored percentages, so it cannot loop. Test: legacy null scalingBasis + MAX_VOLUME prType resolves editor basis to MAX_VOLUME_PR and baseline/per-set weights to the volume PR. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E3r4QJcxvNCB1evniqwxJm --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add stallDetectionEnabled flag to allow users to toggle stall detection auto-stop behavior on Just Lift and AMRAP workout configurations:
Stall detection auto-stops a set when movement pauses for 5 seconds. Users can now choose whether they want this behavior enabled per mode.