more workout types, a filter on the list, and calories that stop guessing - #217
Conversation
…sing The picker had nine types, so boxing, rowing, hiking, skiing, racquet sports and the rest all had to be logged as "Other" and reached Apple Health as an unlabelled workout. There are twenty-one now, each with its own glyph and its own health activity — a type only earns a tile if a pack actually draws it, and a test now fails if a tile is added without a matching health mapping, because that failure is otherwise invisible until someone opens Apple Health and finds a wall of generic entries. That made the start sheet taller than the sheet itself. It is scroll- controlled now; the old guard against content falling off the bottom edge became a guard that every type can still be scrolled to. The list only ever filtered by timeframe. It filters by type, minimum duration and minimum strain too, and sorts by longest or hardest as well as by date. A narrowed list recomputes its own summary rather than showing whole-range totals above it, and a live session is never hidden by a floor it has not had time to clear yet. Calories were the last place the app still made something up. The live tick substituted a 30-year-old 70 kg male for whatever the profile was missing, so an untouched profile still produced a confident total — for somebody else's body, and reading high for anyone lighter than the stand-in. The re-score has always refused to guess; both now share one predicate, and a session with no anchors shows a dash instead of a number.
|
Warning Review limit reached
Next review available in: 48 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (11)
📝 WalkthroughWalkthroughThe change adds profile-based calorie eligibility, preserves unavailable calories as null, expands workout type mappings and icons, and adds workout filtering, sorting, summaries, and scrollable type-selection interfaces. ChangesCalorie availability
Workout discovery
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
actor User
participant WorkoutsScreen
participant WorkoutFilterSheet
participant WorkoutFilter
User->>WorkoutsScreen: Open filters
WorkoutsScreen->>WorkoutFilterSheet: Show current filter and workouts
User->>WorkoutFilterSheet: Edit filters and apply
WorkoutFilterSheet-->>WorkoutsScreen: Return WorkoutFilter
WorkoutsScreen->>WorkoutFilter: Apply filter and sort
WorkoutFilter-->>WorkoutsScreen: Return filtered workouts and summary
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
PR Reviewer Guide 🔍(Review updated until commit a8eb259)Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Latest suggestions up to a8eb259 Explore these optional code suggestions:
Previous suggestionsSuggestions up to commit c0db836
Suggestions up to commit b4bced4
Suggestions up to commit 7d44eb8
Suggestions up to commit ab7a385
|
|
Went through the three flagged spots. The summary total was worth changing. Adding The mid-session profile change can't happen. The navigator one you withdrew yourself on the second look — agreed, the |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/ui/workouts/workout_filter.dart`:
- Around line 91-106: Update _matches to evaluate the explicit types filter
before the live-session early return, so a live workout is retained only when
its canonical type matches; keep the live exception for duration and strain
thresholds. Add a regression test covering types {'run'} and a live cycle
session, asserting it is excluded.
In `@lib/ui/workouts/workout_types.dart`:
- Around line 69-74: Normalize the input once in workoutTypeLabel by storing
type.toLowerCase() in a local key, then compare that key with e.$1 during the
kWorkoutTypes lookup so case variants return the configured label. Add a
regression test covering an uppercase or mixed-case type such as TENNIS and
verifying it returns the mapped Racquet label.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: bb60fd4e-90de-491e-b138-d7dbe5404e68
📒 Files selected for processing (15)
lib/compute/manual_session.dartlib/compute/profile.dartlib/health/health_export.dartlib/state/app_state.dartlib/ui/activity/live_session_screen.dartlib/ui/activity/workout_share_card.dartlib/ui/kit/os_icons.dartlib/ui/workouts/workout_filter.dartlib/ui/workouts/workout_filter_sheet.dartlib/ui/workouts/workout_types.dartlib/ui/workouts/workouts_screen.darttest/workout_calorie_anchors_test.darttest/workout_filter_test.darttest/workout_health_mapping_test.darttest/workouts_header_actions_test.dart
|
Persistent review updated to latest commit 14acd22 |
|
Both of these were right. The live-session exception was too broad. Type is known the moment a session starts, so filtering to runs should never surface the ride that happens to be in progress — only the duration and strain floors need the exception, because a session two minutes old has not had a chance to clear them yet. Split accordingly, with a test for the live-cycle-under-a-run-filter case. The case sensitivity was a real gap, and it was in all three lookups rather than just the label one — the icon lookups compared against the raw string too, so a row written as |
|
Persistent review updated to latest commit ab7a385 |
|
@coderabbitai review |
|
|
The strain sort one was worth taking. Null strain means the session was never scored — no profile anchor, or no heart rate for the window — and ranking that as a genuine zero puts "we could not score this" in among "you barely moved". It sorts below every scored session now, including real zeros. The same question applies to the filter, and there the answer goes the other way: a strain floor excludes an unscored session, because it cannot be shown to clear the bar and listing it under "strain 10+" would be a claim there is no evidence for. Both are pinned by tests. The summary total was already changed in an earlier commit on this branch — it skips a null rather than defaulting it, for exactly the reason described. |
|
Persistent review updated to latest commit 7d44eb8 |
Health Connect's Kotlin write map has SOCCER commented out, but the plugin's Dart-side guard still lists it — so the call reaches the channel and comes back false instead of throwing. This exporter reads a false as a real write failure and counts it toward the day's give-up budget, so one soccer workout would have silently paused that day's entire export, resting HR and sleep included. It maps to OTHER on Android now, and lands unlabelled rather than taking the day with it. The mapping test could not have caught this: its Android set was transcribed from the plugin's Dart list, which is only an advisory pre-check and does not agree with the Kotlin map that actually decides. It pins against the Kotlin map now, and the header says which source is authoritative per platform. A hike records a route, like the walk it sits next to in the picker. Ski and snowboard deliberately do not — pace down a lift-served descent would read as a performance number while measuring gravity. Climb was drawing mountains, directly beside a person hiking. Of those two tiles the mountains were the one that read as hiking.
|
Ran a second pass over this myself, since CodeRabbit's last run came back rate limited rather than actually reviewed. It found one thing that should not have shipped.
The test I added specifically to prevent this class of bug could not have caught it, which is the more useful half of the finding: its Android set was transcribed from the plugin's Dart list, and for Android that is the wrong source of truth. The Dart list is advisory; the Kotlin map is the gate, and here the two disagree. Both sets are now pinned against the file that actually decides, and the header says which one that is per platform. Two smaller things from the same pass. |
|
Persistent review updated to latest commit b4bced4 |
The detail screen — the most-viewed place a session appears — still printed "0 kcal" for a session that was never costed, while the share card exported from that same screen already read "—". A live session with a complete profile but no heart rate at all (link dropped, strap off) accrued nothing and reported a confident 0. That is not a measurement. Calories now track whether the estimate ever ran, so absent and a genuine zero stay distinguishable, and calories agree with strain, which already reported that case as absent. The filter pill could overflow its row. "Basketball, Snowboard · 90m+ · strain 17+" is a reachable description and the header is about 350 pt wide; unconstrained that clips silently in release. It ellipsizes now. A filtered summary was dropping the auto-type accuracy note, which describes the whole range and has nothing to do with the filter. Imported sessions carry the export's own spelling — running, weightlifting, functional fitness — so filtering by Run hid every imported run while the feed cheerfully showed cards titled "Running". There is one alias table now, used by the filter and by every label and icon lookup, so a row cannot read as one type and filter as another.
|
Second self-review pass, this one on correctness. Six findings, five taken and one rejected — the rejected one is the interesting one. Taken. The workout detail screen still printed Rejected, after trying it. The observation that a calorie figure fabricated by an older build is never cleared by a re-score is correct. The obvious fix is to let a complete substrate's null be authoritative instead of falling back to the stored value — I made that change, and So: sessions logged before this change keep whatever the old tick wrote. Everything logged after it is honest, and no migration deletes history on a guess. |
|
Persistent review updated to latest commit c0db836 |
|
Both taken. The summary total was the same fabrication one level up: The two lookup mechanisms could not actually have diverged — |
|
Persistent review updated to latest commit a8eb259 |
|
@coderabbitai review |
|
User description
Five people asked for the same three things in Discord and the discussions, so this is those three.
Types. The picker had nine, which meant boxing, rowing, hiking, skiing, stair work, pilates, racquet sports and the ball sports all had to be logged as "Other" — and reached Apple Health as an unlabelled workout. There are twenty-one now. A type only earns a tile if a pack actually draws it, so nothing ships with a stand-in icon that looks like something else. Adding a tile without adding the matching health mapping is silent — the workout still exports, just as a generic entry — so a test fails on it now.
That pushed the start sheet past the height a bottom sheet gives itself by default, which is exactly how "Log a past workout" once ended up invisible and untappable. The sheet scrolls now, and the old guard against content falling off the edge became a guard that every type can still be reached.
Filtering. The list only ever filtered by timeframe. It now filters by type, by minimum duration and by minimum strain, and sorts by longest or hardest as well as by date. Two details worth naming: a narrowed list recomputes its own summary, so the totals above it always describe what is actually on screen; and a session that is happening right now is never hidden by a floor it has not had time to clear.
Calories. The last place the app still made something up. The live tick substituted a 30-year-old 70 kg male for whatever the profile was missing, so an untouched profile produced a confident calorie total — for somebody else's body, and reading high for anyone lighter than the stand-in. The re-score path has always refused to guess. Both share one predicate now, and a session with no anchors shows a dash rather than a number, all the way out to the shareable card.
PR Type
Enhancement, Bug fix, Tests
Description
Expanded workout types from 9 to 21, each with distinct glyph and health mapping
Added workout list filtering by type, duration, strain, and sort order
Fixed calorie fabrication: unanchored profiles now show "—" instead of a stand-in body's estimate
Added tests for calorie anchors, filter logic, and health mapping coverage
Diagram Walkthrough
File Walkthrough
5 files
Add hasCalorieAnchors predicate for Keytel formulaGate substrate re-score calories on hasCalorieAnchorsRemove fallback defaults from live calorie tick, use caloriesOrNullPropagate nullable calories through finish card and shareAccept nullable calories, dash out on share card6 files
Add 12 new sport glyphs for expanded workout typesExpand picker from 9 to 21 types, fix scrollable sheetNew filter/sort policy for workout listNew bottom sheet UI for filter and sort controlsWire filter bar, recompute summary over filtered listMap 13 new workout types to platform health activities4 files
Tests for hasCalorieAnchors and caloriesOrNull contractUnit tests for filter, sort, and summarize logicExtend health mapping tests for all new typesUpdate sheet test to assert scrollable grid reachabilitySummary by CodeRabbit
New Features
Bug Fixes