feat: add workout summary screen and enhance app theme colors - #50
Conversation
Adds the read side of Health Connect and a home-screen readiness card: - IHealthConnectService: read permissions + sleep/RHR/HRV/HR queries - ReadinessCalculator: pure scoring vs personal 14-day baseline (sleep 0.5 / resting HR 0.3 / HRV 0.2, renormalized over available components; only adverse deviation penalized; 5-sample minimum) - ReadinessManager: daily baseline cache + 30-min snapshot TTL in settings storage; every failure degrades to noData, never throws - ReadinessCard on dashboard (self-hides without data) with details bottom sheet; opt-in toggle in profile Health Connect section - New READ_SLEEP / READ_HEART_RATE / READ_RESTING_HEART_RATE / READ_HEART_RATE_VARIABILITY manifest permissions https://claude.ai/code/session_01FTgsHTfbvXsxwTUe74UrYe
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughThis PR introduces a comprehensive UI refresh and feature expansion for the RepForge workout tracking app. It adds conversational AI coaching capabilities with Gemini integration, redesigns the home dashboard with insights and analytics, implements a routine optimizer screen, refreshes the history/analytics/exercise library interfaces, adds first-launch onboarding, introduces personal records tracking, and updates Android build configuration to SDK 36 with manifest health permissions. ChangesUI Refresh and Feature Implementation
Possibly Related PRs
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## Revamp-of-UI #50 +/- ##
================================================
+ Coverage 41.56% 41.74% +0.18%
================================================
Files 68 71 +3
Lines 11457 11896 +439
================================================
+ Hits 4762 4966 +204
- Misses 6695 6930 +235 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 28
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
workout-logger/lib/main.dart (1)
93-93: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick winHoist
ApiServiceout ofbuild().
Provider<ApiService>.value(value: ApiService())creates a fresh service every timeWorkoutLoggerApprebuilds, which breaks the composition-root singleton assumption used by the rest of this file.Suggested fix
static final SettingsProvider _settingsProvider = SettingsProvider(_storageService); + static final ApiService _apiService = ApiService(); // HealthSyncManager uses the in-memory settings flag — no storage I/O on sync. static final HealthSyncManager _healthSyncManager = HealthSyncManager(_healthConnectService, _settingsProvider); @@ - Provider<ApiService>.value(value: ApiService()), + Provider<ApiService>.value(value: _apiService),Based on learnings, "Wire all services through dependency injection in main.dart via AppInitializer using constructor injection".
🤖 Prompt for 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. In `@workout-logger/lib/main.dart` at line 93, The Provider is instantiating ApiService inside WorkoutLoggerApp.build which recreates the service on every rebuild; instead construct a single ApiService once (hoist it out of build) and inject it via your AppInitializer/constructor injection so the app uses a singleton instance. Locate the Provider<ApiService>.value(value: ApiService()) entry and replace it so it references the hoisted ApiService instance provided by AppInitializer (or passed into WorkoutLoggerApp's constructor), ensuring ApiService is created only once and wired through the DI root.Source: Learnings
workout-logger/lib/screens/programs/program_designer_screen.dart (1)
660-676: 🧹 Nitpick | 🔵 Trivial | 💤 Low valueConsider performance impact of
shrinkWrap: true.Line 666 adds
shrinkWrap: trueto aListView.builderinside a dialog. While necessary for theConstrainedBoxto work correctly,shrinkWrapcan have performance implications if the list is long. Given that the filtered list is limited to 20 items (line 641:.take(20)), this should be acceptable, but document this constraint if it's critical.🤖 Prompt for 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. In `@workout-logger/lib/screens/programs/program_designer_screen.dart` around lines 660 - 676, Add a brief inline comment next to the ListView.builder/ConstrainedBox explaining why shrinkWrap: true is used and that the list is intentionally bounded (filtered is limited via .take(20)), so the performance cost is acceptable; reference the ListView.builder, shrinkWrap, ConstrainedBox and the filtered.take(20) limitation to make the rationale clear for future maintainers.
🤖 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 `@CLAUDE.md`:
- Around line 298-301: Update the CLAUDE.md prerequisite so the instruction
about reading graphify-out/GRAPH_REPORT.md is conditional: change the absolute
"ALWAYS read graphify-out/GRAPH_REPORT.md" statement to check for the file's
existence and provide a fallback (e.g., "If graphify-out/GRAPH_REPORT.md exists,
read it first; otherwise use the project's README or run `graphify update .`
and/or `graphify init` to generate the graph") and adjust the subsequent
guidance (the wiki/index.md note and graphify commands) to reference the same
conditional flow; modify the sentence containing "ALWAYS read
graphify-out/GRAPH_REPORT.md" and related lines in CLAUDE.md to implement this
fallback wording.
In `@workout-logger/lib/main.dart`:
- Around line 180-184: The check that computes versionChanged incorrectly treats
a null settings.lastSeenVersion as "seen"; update the logic that sets
versionChanged (near the use of settings.getCurrentVersion(), settings.userName,
and settings.lastSeenVersion) so that if lastSeenVersion is null it counts as a
change/unseen—i.e., versionChanged should be true when the user has a name
(needsName is false) and (settings.lastSeenVersion is null OR
settings.lastSeenVersion != version); update the versionChanged expression
accordingly.
In `@workout-logger/lib/models/models.dart`:
- Around line 327-330: Routine.copyWith currently preserves the original List
reference for exerciseIds causing shared mutable state; update Routine.copyWith
to defensively clone list-backed fields (e.g., use exerciseIds == null ?
List.from(this.exerciseIds) : List.from(exerciseIds) or .toList()) when
constructing the new Routine so the new instance gets its own list. Apply the
same defensive copying pattern to the other model copyWith methods mentioned
(the copyWith implementations around the other model blocks referenced in the
comment) so no list-backed field retains the original mutable reference.
In `@workout-logger/lib/screens/ai_coach_screen.dart`:
- Line 382: The call to AppColors.surface.withValues uses the withValues API;
update the call at AppColors.surface.withValues so it uses the correct named
parameter syntax and a valid alpha in the 0.0–1.0 range (e.g., alpha: 0.9 for
90% opacity); if your intention was a different opacity (e.g., 10% transparent),
adjust the numeric value accordingly and keep the named parameter alpha to match
the method signature.
- Around line 817-840: The _AiAvatar StatelessWidget lacks a const constructor;
add a const constructor like "const _AiAvatar({Key? key}) : super(key: key);" to
the _AiAvatar class and ensure its fields (if any) are final/immutable, then
update instantiations of _AiAvatar to use const where possible so the widget can
be created as a compile-time constant.
In `@workout-logger/lib/screens/ai_program_generator_screen.dart`:
- Line 50: Extract the error message into a local variable (e.g., final String
msg = 'Add your Gemini API key in Profile → AI Features first.') and then call
setState with a simple, single-statement assignment that uses that variable
(reference _error and setState) instead of embedding the full string expression
inline; update the code around the setState call that assigns to _error so it
becomes more readable and easier to refactor later.
In `@workout-logger/lib/screens/analytics_screen.dart`:
- Around line 86-134: Replace the hardcoded BorderRadius.circular(14) and
BorderRadius.circular(11) in _buildPillTabBar with the design-system radius
tokens from AppRadius (e.g., AppRadius.large for the container and
AppRadius.small for the pill) so the Container decoration and the
AnimatedContainer decoration use BorderRadius.circular(AppRadius.<token>)
instead of numeric literals; update both occurrences and ensure AppRadius is
imported/available.
In `@workout-logger/lib/screens/edit_workout_session_screen.dart`:
- Around line 390-395: The onSetChanged handler (the callback updating log.sets
in EditWorkoutSessionScreen) fails to clear stale drop entries when isDropset
flips false; update the handler inside setState so that when isDropset is false
you explicitly set log.sets[setIndex].drops = null (or empty list per
WorkoutSet.drops contract) in addition to updating weight/reps/isDropset,
ensuring _save() will not serialize old drops for non-dropset rows.
In `@workout-logger/lib/screens/exercise_library_screen.dart`:
- Around line 201-225: The TextField is uncontrolled, so clearing the suffix
icon only updates _query but not the visible text; make the field controlled by
creating a TextEditingController in the State (e.g., _searchController), pass
controller: _searchController into the TextField, wire the controller to the
existing onChanged callback (keep onChanged: onChanged), and in the suffix
GestureDetector onTap call _searchController.clear() and then onChanged('') to
keep UI and state in sync; also ensure you initialize the controller (optionally
with initial text) and dispose it in dispose().
- Around line 483-495: The tap handler for ExerciseCard is mutating _selectedIds
regardless of selectionMode; change the onTap callback in the widget builder to
first check selectionMode and only call setState and toggle _selectedIds when
selectionMode is true (leave taps untouched when selectionMode is false). Update
the code near the ExerciseCard creation where sel is computed from _selectedIds
and the onTap lambda is defined so that the toggle logic is gated by the
selectionMode boolean to avoid entering a hidden selection state.
In `@workout-logger/lib/screens/history_screen.dart`:
- Around line 307-320: The calendar nav buttons use a hardcoded
BorderRadius.circular(8); replace that with the design token (e.g.,
AppRadius.someRadius) in the Container decoration for the left and right
navigation buttons so they use the design system. Locate the
GestureDetector/Container widgets that update _calendarMonth and _selectedDay
(the left chevron button with Icon(Icons.chevron_left_rounded) and the
corresponding right chevron) and swap BorderRadius.circular(8) for the
appropriate AppRadius token used elsewhere in the app.
- Around line 236-260: The Container's decoration uses a hardcoded
borderRadius.circular(10); replace that literal with the design token from
AppRadius (e.g., use AppRadius.small) so the GestureDetector/Container
decoration consistently uses the design system token (update
BorderRadius.circular(10) to BorderRadius.circular(AppRadius.small) or
equivalent AppRadius token and add any necessary import).
- Around line 434-455: The Container decoration uses a hardcoded borderRadius:
BorderRadius.circular(12); replace this with the design token from your radius
system (e.g. use AppRadius.<appropriateToken>) so the code becomes borderRadius:
BorderRadius.circular(AppRadius.<token>) or
BorderRadius.all(Radius.circular(AppRadius.<token>)); update the instance in
history_screen.dart (the Container/TextField search bar decoration) to reference
the AppRadius token instead of 12.
In `@workout-logger/lib/screens/home_screen.dart`:
- Around line 464-507: Replace the hardcoded BorderRadius.circular(12) in the
Start/Resume button's Container decoration with the design system token (e.g.,
AppRadius.medium or the appropriate AppRadius constant) so the
GestureDetector/Container uses the AppRadius token for borderRadius; update the
BoxDecoration in the widget where borderRadius is set to reference AppRadius
instead of BorderRadius.circular(12) and ensure imports for the design tokens
are present if needed.
- Around line 308-328: Replace the hardcoded BorderRadius.circular(12) used in
the Container inside the GestureDetector (the profile button) with the
design-system radius token from AppRadius (e.g., AppRadius.* appropriate token)
so the widget uses the shared radius values; update the Container's decoration
to use BorderRadius.circular(...) -> BorderRadius.all(AppRadius.<token>) or
directly the token shape as your design system expects, ensuring the rest of the
properties (color, border) remain unchanged and the ProfileScreen navigation and
Icon usage are not modified.
- Around line 278-306: Replace the hardcoded borderRadius.circular(12) in the AI
coach button Container decoration with the design token from the app's radius
tokens (use AppRadius.small instead of 12) so the snippet uses
BorderRadius.circular(AppRadius.small) (update the Container/BoxDecoration in
the HomeScreen build where borderRadius.circular(12) is used for the AI coach
button that navigates to AiCoachScreen).
In `@workout-logger/lib/screens/onboarding_screen.dart`:
- Around line 61-156: The UI uses hardcoded spacing and padding values (28, 12,
16, 14) inside the Column and TextField decorations instead of the design
tokens; update those literals to use the appropriate tokens (e.g., replace
horizontal padding 28 with AppSpacing.[sm/md/lg] or a consistent token, replace
SizedBox height 12 with AppSpacing.[xs/sm], and replace contentPadding
horizontal/vertical 16/14 with AppSpacing tokens) to match the design system and
keep existing usage of AppRadius and AppSpacing elsewhere; ensure changes are
applied around the Container/Padding/ SizedBox/TextField (referencing
_controller, TextField, AppRadius, AppSpacing) so spacing is consistent across
the onboarding_screen.dart.
In `@workout-logger/lib/screens/profile_screen.dart`:
- Around line 205-207: The code treats ShareResultStatus.dismissed as a
successful export; change the condition so only ShareResultStatus.success
triggers _showSnack('Backup exported successfully!', AppColors.success) and
handle ShareResultStatus.dismissed separately (e.g., call _showSnack with a
canceled/neutral message and AppColors.warning or do nothing) so users aren't
misled that a backup left the app; update the check of result.status (and any
surrounding export/share function) to only consider ShareResultStatus.success as
success and add an else-if for ShareResultStatus.dismissed to show an
appropriate cancellation message.
In `@workout-logger/lib/screens/routine_optimizer_screen.dart`:
- Around line 473-497: The _OptimizerAvatar StatelessWidget lacks a const
constructor; add a const constructor (const _OptimizerAvatar({Key? key}) :
super(key: key);) to the _OptimizerAvatar class and update all places that
instantiate _OptimizerAvatar to use const where applicable so the widget can be
const-constructed and leverage compile-time optimizations.
In `@workout-logger/lib/screens/routines_screen.dart`:
- Around line 494-518: The current onTap handler in the GestureDetector blocks
navigation to RoutineOptimizerScreen when sessionCount < 3; make the threshold
configurable and/or change the behavior to warn instead of blocking: extract the
hardcoded "3" into a configurable constant or a user setting (e.g.,
optimizerMinSessions) and reference it where sessionCount is computed, and if
you prefer a soft warning, replace the early return with a SnackBar warning that
still calls Navigator.push to RoutineOptimizerScreen(routine: routine) (or add
an override confirmation dialog) so users can proceed after 1–2 sessions if they
choose.
In `@workout-logger/lib/screens/widgets/activity_heatmap.dart`:
- Around line 23-41: Replace the hardcoded spacing numbers in GridView.builder's
SliverGridDelegateWithFixedCrossAxisCount with the design system tokens (use
AppSpacing.small for crossAxisSpacing and mainAxisSpacing) so spacing comes from
the design system; update the SliverGridDelegateWithFixedCrossAxisCount
constructor in the activity_heatmap.dart GridView.builder (references:
GridView.builder, SliverGridDelegateWithFixedCrossAxisCount, crossAxisSpacing,
mainAxisSpacing) to use AppSpacing tokens instead of the literal 3.
In `@workout-logger/lib/screens/widgets/analytics_overview.dart`:
- Around line 361-403: Replace the hardcoded BorderRadius.circular(10) and
BorderRadius.circular(7) in the _RangeToggle widget with the design-system
radius tokens (use the appropriate AppRadius token(s) from your design system,
e.g. AppRadius.medium/AppRadius.small) so both the container decoration and the
inner AnimatedContainer use AppRadius instead of numeric literals; update the
BoxDecoration.borderRadius in _RangeToggle and the borderRadius on the inner
AnimatedContainer to reference the AppRadius token(s).
In `@workout-logger/lib/screens/widgets/calendar_grid.dart`:
- Around line 58-67: The SliverGridDelegateWithFixedCrossAxisCount in
GridView.builder currently uses hardcoded spacing (crossAxisSpacing: 4,
mainAxisSpacing: 4); replace those literals with the design system tokens (e.g.,
AppSpacing.small or the appropriate AppSpacing constant) so both
crossAxisSpacing and mainAxisSpacing reference AppSpacing instead of numeric
values; update the GridView.builder/SliverGridDelegateWithFixedCrossAxisCount
instantiation accordingly to use the token names.
- Around line 188-203: Replace the hardcoded borderRadius.circular(8) inside the
Container's BoxDecoration (in the day cell widget returned by GestureDetector)
with the design-system token (e.g., AppRadius.small) so it uses the AppRadius
value instead of a magic number; update the borderRadius property to
BorderRadius.circular(AppRadius.small) or the appropriate AppRadius token to
match the design system.
In `@workout-logger/lib/screens/widgets/editable_exercise_card.dart`:
- Around line 331-352: The weight input is hardcoded to 'kg' in the _NumField
(using _weightCtrl/_weightFocus and onWeightChanged), causing wrong
display/edits in lb mode; update the suffix to read the unit label from
SettingsProvider (or equivalent settings getter) and ensure displayed value and
parsing honor the current unit by converting stored kg to the user unit when
initializing _weightCtrl.text and converting back to kg before calling
widget.onWeightChanged; apply the same change for the other occurrence around
the 531-554 block so both editor rows use the settings-based unit and proper
conversions.
- Around line 18-45: EditableExerciseLog and EditableSet are mutable but must be
immutable and updated via copyWith; change all fields on EditableExerciseLog and
EditableSet to final, make constructors constant/positional as appropriate,
ensure lists (sets and drops) are stored as unmodifiable (e.g.,
List.unmodifiable) and then add copyWith methods on both classes
(EditableExerciseLog.copyWith and EditableSet.copyWith) that let callers replace
fields or produce modified copies of nested lists (e.g., return a new
EditableExerciseLog with a modified sets list where an EditableSet is replaced
via its copyWith, and EditableSet.copyWith should allow updating weight, reps,
isDropset, drops, timeTaken, and timestamp). Update any code that previously
mutated fields directly to use these copyWith helpers and to replace lists
atomically rather than mutating in place.
In `@workout-logger/lib/screens/widgets/exercise_details_sheet.dart`:
- Around line 233-244: The Text currently hardcodes a '+' before the slope which
causes "+-..." when growthModel.slope is negative; update the widget that builds
the label (the Text containing
'+${settings.toDisplay(growthModel.slope).toStringAsFixed(1)}
${settings.unitLabel} volume/session') to prepend '+' only when
growthModel.slope > 0 (e.g., compute a sign = growthModel.slope > 0 ? '+' : ''
and render "$sign${settings.toDisplay(growthModel.slope).toStringAsFixed(1)}
${settings.unitLabel} volume/session"), keeping the existing settings.toDisplay
and unitLabel usage so negative values retain their '-' sign.
In `@workout-logger/lib/screens/widgets/exercise_input_section.dart`:
- Around line 877-892: The deload banner is using hardcoded Colors.amber in two
places; replace those with the design token from AppColors (e.g.,
AppColors.amber or the project's deload/warning token) so the theme system is
used consistently. Update the Border.all color branch (where it currently uses
week.isDeload ? Colors.amber.withValues(alpha: 0.4) ...) and the Icon color
(Icon(..., color: Colors.amber)) to use the matching AppColors token and its
withValues(alpha: ...) variant; keep the week.isDeload condition and existing
sizing/padding intact.
---
Outside diff comments:
In `@workout-logger/lib/main.dart`:
- Line 93: The Provider is instantiating ApiService inside
WorkoutLoggerApp.build which recreates the service on every rebuild; instead
construct a single ApiService once (hoist it out of build) and inject it via
your AppInitializer/constructor injection so the app uses a singleton instance.
Locate the Provider<ApiService>.value(value: ApiService()) entry and replace it
so it references the hoisted ApiService instance provided by AppInitializer (or
passed into WorkoutLoggerApp's constructor), ensuring ApiService is created only
once and wired through the DI root.
In `@workout-logger/lib/screens/programs/program_designer_screen.dart`:
- Around line 660-676: Add a brief inline comment next to the
ListView.builder/ConstrainedBox explaining why shrinkWrap: true is used and that
the list is intentionally bounded (filtered is limited via .take(20)), so the
performance cost is acceptable; reference the ListView.builder, shrinkWrap,
ConstrainedBox and the filtered.take(20) limitation to make the rationale clear
for future maintainers.
🪄 Autofix (Beta)
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
Run ID: 7e85d8a3-0135-4882-996b-c369b7b0bb54
📒 Files selected for processing (98)
.gitignoreCLAUDE.mddocs/superpowers/specs/2026-06-08-conversational-routine-optimizer-design.mdworkout-logger/RELEASE_NOTES.mdworkout-logger/android/app/build.gradle.ktsworkout-logger/android/app/src/main/AndroidManifest.xmlworkout-logger/android/app/src/main/kotlin/com/workoutlogger/workout_logger/MainActivity.ktworkout-logger/lib/main.dartworkout-logger/lib/models/models.dartworkout-logger/lib/screens/add_custom_exercise_screen.dartworkout-logger/lib/screens/ai_coach_screen.dartworkout-logger/lib/screens/ai_program_generator_screen.dartworkout-logger/lib/screens/analytics_screen.dartworkout-logger/lib/screens/edit_workout_session_screen.dartworkout-logger/lib/screens/exercise_library_screen.dartworkout-logger/lib/screens/history_screen.dartworkout-logger/lib/screens/home_screen.dartworkout-logger/lib/screens/onboarding_screen.dartworkout-logger/lib/screens/profile_screen.dartworkout-logger/lib/screens/programs/program_designer_screen.dartworkout-logger/lib/screens/programs/program_detail_screen.dartworkout-logger/lib/screens/programs/programs_screen.dartworkout-logger/lib/screens/routine_optimizer_screen.dartworkout-logger/lib/screens/routines_screen.dartworkout-logger/lib/screens/widgets/activity_heatmap.dartworkout-logger/lib/screens/widgets/analytics_overview.dartworkout-logger/lib/screens/widgets/body_heatmap.dartworkout-logger/lib/screens/widgets/calendar_grid.dartworkout-logger/lib/screens/widgets/dashboard_widgets.dartworkout-logger/lib/screens/widgets/editable_exercise_card.dartworkout-logger/lib/screens/widgets/exercise_details_sheet.dartworkout-logger/lib/screens/widgets/exercise_input_section.dartworkout-logger/lib/screens/widgets/exercise_progress_view.dartworkout-logger/lib/screens/widgets/muscle_detail_sheet.dartworkout-logger/lib/screens/widgets/profile_sections.dartworkout-logger/lib/screens/widgets/program_week_editor.dartworkout-logger/lib/screens/widgets/program_week_tile.dartworkout-logger/lib/screens/widgets/readiness_card.dartworkout-logger/lib/screens/widgets/rest_timer_view.dartworkout-logger/lib/screens/widgets/rf_cards.dartworkout-logger/lib/screens/widgets/rf_inputs.dartworkout-logger/lib/screens/widgets/rf_question_card.dartworkout-logger/lib/screens/widgets/rf_widgets.dartworkout-logger/lib/screens/widgets/routine_creator.dartworkout-logger/lib/screens/widgets/session_details_sheet.dartworkout-logger/lib/screens/widgets/sparkline_painter.dartworkout-logger/lib/screens/widgets/targets_tab.dartworkout-logger/lib/screens/widgets/volume_chart.dartworkout-logger/lib/screens/widgets/wheel_picker.dartworkout-logger/lib/screens/widgets/workout_header.dartworkout-logger/lib/screens/workout_flow_screen.dartworkout-logger/lib/screens/workout_summary_screen.dartworkout-logger/lib/services/ai/coach_tool_service.dartworkout-logger/lib/services/ai/gemini_ai_service.dartworkout-logger/lib/services/gemini_context_builder.dartworkout-logger/lib/services/health_connect_service.dartworkout-logger/lib/services/interfaces/ai_service_interface.dartworkout-logger/lib/services/interfaces/health_connect_service_interface.dartworkout-logger/lib/services/interfaces/interfaces.dartworkout-logger/lib/services/interfaces/ml_service_interface.dartworkout-logger/lib/services/interfaces/readiness_manager_interface.dartworkout-logger/lib/services/interfaces/storage_service_interface.dartworkout-logger/lib/services/managers/conversation_manager.dartworkout-logger/lib/services/managers/managers.dartworkout-logger/lib/services/managers/pr_manager.dartworkout-logger/lib/services/managers/readiness_manager.dartworkout-logger/lib/services/ml_service.dartworkout-logger/lib/services/settings_provider.dartworkout-logger/lib/services/storage_service.dartworkout-logger/lib/services/utils/readiness_calculator.dartworkout-logger/lib/services/workout_provider.dartworkout-logger/lib/theme/app_theme.dartworkout-logger/lib/viewmodels/ai_coach_view_model.dartworkout-logger/lib/viewmodels/routine_optimizer_view_model.dartworkout-logger/pubspec.yamlworkout-logger/test/add_custom_exercise_screen_test.dartworkout-logger/test/ai_coach_view_model_test.dartworkout-logger/test/analytics_manager_test.dartworkout-logger/test/analytics_queries_test.dartworkout-logger/test/analytics_screen_test.dartworkout-logger/test/coach_tool_service_test.dartworkout-logger/test/conversation_manager_test.dartworkout-logger/test/exercise_library_screen_test.dartworkout-logger/test/exercise_progress_view_test.dartworkout-logger/test/gemini_ai_service_usage_test.dartworkout-logger/test/health_sync_manager_test.dartworkout-logger/test/history_manager_test.dartworkout-logger/test/ml_service_test.dartworkout-logger/test/model_serialization_test.dartworkout-logger/test/pr_manager_test.dartworkout-logger/test/readiness_calculator_test.dartworkout-logger/test/readiness_manager_test.dartworkout-logger/test/rf_question_card_test.dartworkout-logger/test/routine_optimizer_screen_test.dartworkout-logger/test/routine_optimizer_view_model_test.dartworkout-logger/test/test_utils/mock_ml_service.dartworkout-logger/test/test_utils/mock_storage_service.dartworkout-logger/test/workout_provider_test.dart
💤 Files with no reviewable changes (1)
- workout-logger/android/app/src/main/kotlin/com/workoutlogger/workout_logger/MainActivity.kt
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
🛑 Comments failed to post (28)
CLAUDE.md (1)
298-301:
⚠️ Potential issue | 🟠 Major | ⚡ Quick winMake the graphify prerequisite conditional to avoid dead-end instructions.
Line 298 says to always read
graphify-out/GRAPH_REPORT.mdbefore any source inspection, but.gitignore(Line 83 in.gitignore) excludesgraphify-out/, so this file may not exist in clean checkouts. Please make this rule conditional with a fallback path.Proposed doc fix
-- ALWAYS read graphify-out/GRAPH_REPORT.md before reading any source files, running grep/glob searches, or answering codebase questions. The graph is your primary map of the codebase. +- IF graphify-out/GRAPH_REPORT.md exists, read it first and use it as the primary map. If it does not exist, proceed with normal source inspection.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.- IF graphify-out/GRAPH_REPORT.md exists, read it first and use it as the primary map. If it does not exist, proceed with normal source inspection. - IF graphify-out/wiki/index.md EXISTS, navigate it instead of reading raw files - For cross-module "how does X relate to Y" questions, prefer `graphify query "<question>"`, `graphify path "<A>" "<B>"`, or `graphify explain "<concept>"` over grep — these traverse the graph's EXTRACTED + INFERRED edges instead of scanning files - After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost).🤖 Prompt for 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. In `@CLAUDE.md` around lines 298 - 301, Update the CLAUDE.md prerequisite so the instruction about reading graphify-out/GRAPH_REPORT.md is conditional: change the absolute "ALWAYS read graphify-out/GRAPH_REPORT.md" statement to check for the file's existence and provide a fallback (e.g., "If graphify-out/GRAPH_REPORT.md exists, read it first; otherwise use the project's README or run `graphify update .` and/or `graphify init` to generate the graph") and adjust the subsequent guidance (the wiki/index.md note and graphify commands) to reference the same conditional flow; modify the sentence containing "ALWAYS read graphify-out/GRAPH_REPORT.md" and related lines in CLAUDE.md to implement this fallback wording.workout-logger/lib/main.dart (1)
180-184:
⚠️ Potential issue | 🟠 Major | ⚡ Quick winTreat a missing
lastSeenVersionas unseen.Any existing install that already has a name but has never persisted
lastSeenVersionwill evaluateversionChangedtofalsehere, so the new update sheet never appears for that user on this rollout.Suggested fix
- final versionChanged = !needsName && - settings.lastSeenVersion != null && - settings.lastSeenVersion != version; + final versionChanged = + !needsName && settings.lastSeenVersion != version;🤖 Prompt for 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. In `@workout-logger/lib/main.dart` around lines 180 - 184, The check that computes versionChanged incorrectly treats a null settings.lastSeenVersion as "seen"; update the logic that sets versionChanged (near the use of settings.getCurrentVersion(), settings.userName, and settings.lastSeenVersion) so that if lastSeenVersion is null it counts as a change/unseen—i.e., versionChanged should be true when the user has a name (needsName is false) and (settings.lastSeenVersion is null OR settings.lastSeenVersion != version); update the versionChanged expression accordingly.workout-logger/lib/models/models.dart (1)
327-330: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Defensively copy list-backed model fields.
These paths keep mutable list references alive across old/new model instances. A later
add/removeon the copied object will mutate state outside the intendedcopyWith()flow.Suggested fix
Routine copyWith({String? name, List<String>? exerciseIds}) => Routine( id: id, name: name ?? this.name, - exerciseIds: exerciseIds ?? this.exerciseIds, + exerciseIds: List<String>.from(exerciseIds ?? this.exerciseIds), createdAt: createdAt, ); @@ Conversation({ String? id, required this.title, this.kind = 'coach', DateTime? createdAt, DateTime? updatedAt, List<ChatMessage>? messages, }) : id = id ?? _uuid.v4(), createdAt = createdAt ?? DateTime.now(), updatedAt = updatedAt ?? createdAt ?? DateTime.now(), - messages = messages ?? const []; + messages = List<ChatMessage>.unmodifiable(messages ?? const []); @@ updatedAt: updatedAt == _sentinel ? this.updatedAt : updatedAt as DateTime, - messages: messages == _sentinel - ? this.messages - : messages as List<ChatMessage>, + messages: List<ChatMessage>.unmodifiable( + messages == _sentinel ? this.messages : messages as List<ChatMessage>, + ), );As per coding guidelines, "All model mutations must go through
copyWith()— never mutate state directly".Also applies to: 875-885, 915-923
🤖 Prompt for 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. In `@workout-logger/lib/models/models.dart` around lines 327 - 330, Routine.copyWith currently preserves the original List reference for exerciseIds causing shared mutable state; update Routine.copyWith to defensively clone list-backed fields (e.g., use exerciseIds == null ? List.from(this.exerciseIds) : List.from(exerciseIds) or .toList()) when constructing the new Routine so the new instance gets its own list. Apply the same defensive copying pattern to the other model copyWith methods mentioned (the copyWith implementations around the other model blocks referenced in the comment) so no list-backed field retains the original mutable reference.Source: Coding guidelines
workout-logger/lib/screens/ai_coach_screen.dart (2)
382-382:
⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUse the correct
withValuesnamed parameter.Line 382 uses
.withValues(alpha: 0.9), but the correct Dart 3 API is.withValues(alpha: 0.9)with explicitly named parameters. However, thewithValuesmethod signature expects analphavalue between 0.0 and 1.0. Verify this is correctly expressing "90% opacity" - if you meant 90% opacity, the value should remain0.9. If this should be fully opaque with a 10% reduction, clarify the intent.🤖 Prompt for 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. In `@workout-logger/lib/screens/ai_coach_screen.dart` at line 382, The call to AppColors.surface.withValues uses the withValues API; update the call at AppColors.surface.withValues so it uses the correct named parameter syntax and a valid alpha in the 0.0–1.0 range (e.g., alpha: 0.9 for 90% opacity); if your intention was a different opacity (e.g., 10% transparent), adjust the numeric value accordingly and keep the named parameter alpha to match the method signature.Source: Coding guidelines
817-840:
⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd
constconstructor to_AiAvatar.The
_AiAvatarwidget at line 816 is missing aconstconstructor. As per coding guidelines, useconstconstructors wherever possible in Dart/Flutter code.🔧 Proposed fix
class _AiAvatar extends StatelessWidget { + const _AiAvatar({super.key}); + `@override` Widget build(BuildContext context) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.class _AiAvatar extends StatelessWidget { const _AiAvatar({super.key}); `@override` Widget build(BuildContext context) { return Container( width: 28, height: 28, decoration: BoxDecoration( gradient: const LinearGradient( colors: [AppColors.primary, Color(0xFF5B21B6)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(AppRadius.sm), boxShadow: [ BoxShadow( color: AppColors.primaryGlow(0.35), blurRadius: 8, spreadRadius: -2, ), ], ), child: const Icon(Icons.auto_awesome_rounded, color: Colors.white, size: 14), ); } }🤖 Prompt for 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. In `@workout-logger/lib/screens/ai_coach_screen.dart` around lines 817 - 840, The _AiAvatar StatelessWidget lacks a const constructor; add a const constructor like "const _AiAvatar({Key? key}) : super(key: key);" to the _AiAvatar class and ensure its fields (if any) are final/immutable, then update instantiations of _AiAvatar to use const where possible so the widget can be created as a compile-time constant.Source: Coding guidelines
workout-logger/lib/screens/ai_program_generator_screen.dart (1)
50-50: 🧹 Nitpick | 🔵 Trivial | 💤 Low value
Avoid inline setState with complex expressions.
Line 50 contains a complex inline setState with a compound assignment. This reduces readability. Consider extracting the assignment to a local variable first.
♻️ Optional refactor
if (!gemini.isConfigured) { - setState(() { _error = 'Add your Gemini API key in Profile → AI Features first.'; }); + final errorMsg = 'Add your Gemini API key in Profile → AI Features first.'; + setState(() => _error = errorMsg); return; }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.if (!gemini.isConfigured) { final errorMsg = 'Add your Gemini API key in Profile → AI Features first.'; setState(() => _error = errorMsg); return; }🤖 Prompt for 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. In `@workout-logger/lib/screens/ai_program_generator_screen.dart` at line 50, Extract the error message into a local variable (e.g., final String msg = 'Add your Gemini API key in Profile → AI Features first.') and then call setState with a simple, single-statement assignment that uses that variable (reference _error and setState) instead of embedding the full string expression inline; update the code around the setState call that assigns to _error so it becomes more readable and easier to refactor later.workout-logger/lib/screens/analytics_screen.dart (1)
86-134: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Use design system tokens for border radius.
The pill tab bar uses hardcoded border radius values (
14and11) instead ofAppRadiustokens. As per coding guidelines, always use design system tokens rather than hardcoded values.♻️ Proposed fix
child: Container( padding: const EdgeInsets.all(3), decoration: BoxDecoration( color: AppColors.glass2, - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(AppRadius.lg), border: Border.all(color: AppColors.glassBorder), ), child: Row( children: List.generate(_tabs.length, (i) { final active = i == _tab; return Expanded( child: GestureDetector( onTap: () => setState(() => _tab = i), child: AnimatedContainer( duration: const Duration(milliseconds: 200), curve: Curves.easeOut, padding: const EdgeInsets.symmetric(vertical: 8), decoration: BoxDecoration( color: active ? AppColors.primary : Colors.transparent, - borderRadius: BorderRadius.circular(11), + borderRadius: BorderRadius.circular(AppRadius.md),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.Widget _buildPillTabBar() { return Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), child: Container( padding: const EdgeInsets.all(3), decoration: BoxDecoration( color: AppColors.glass2, borderRadius: BorderRadius.circular(AppRadius.lg), border: Border.all(color: AppColors.glassBorder), ), child: Row( children: List.generate(_tabs.length, (i) { final active = i == _tab; return Expanded( child: GestureDetector( onTap: () => setState(() => _tab = i), child: AnimatedContainer( duration: const Duration(milliseconds: 200), curve: Curves.easeOut, padding: const EdgeInsets.symmetric(vertical: 8), decoration: BoxDecoration( color: active ? AppColors.primary : Colors.transparent, borderRadius: BorderRadius.circular(AppRadius.md), boxShadow: active ? [ BoxShadow( color: AppColors.primary.withValues(alpha: 0.35), blurRadius: 12, ), ] : null, ), child: Text( _tabs[i], textAlign: TextAlign.center, style: GoogleFonts.geist( fontSize: 13, fontWeight: FontWeight.w600, color: active ? Colors.white : AppColors.textMuted, ), ), ), ), ); }), ), ), ); }🤖 Prompt for 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. In `@workout-logger/lib/screens/analytics_screen.dart` around lines 86 - 134, Replace the hardcoded BorderRadius.circular(14) and BorderRadius.circular(11) in _buildPillTabBar with the design-system radius tokens from AppRadius (e.g., AppRadius.large for the container and AppRadius.small for the pill) so the Container decoration and the AnimatedContainer decoration use BorderRadius.circular(AppRadius.<token>) instead of numeric literals; update both occurrences and ensure AppRadius is imported/available.Source: Coding guidelines
workout-logger/lib/screens/edit_workout_session_screen.dart (1)
390-395:
⚠️ Potential issue | 🟠 Major | ⚡ Quick winClear hidden drop entries when a set stops being a dropset.
This handler never nulls
log.sets[setIndex].dropswhenisDropsetflips tofalse._save()then serializes those stale drop entries intoWorkoutSet.drops, even though the user turned the dropset off.Suggested fix
onSetChanged: ({required int setIndex, required double weight, required int reps, required bool isDropset, List<DropsetEntry>? drops}) { setState(() { - log.sets[setIndex].weight = weight; - log.sets[setIndex].reps = reps; - log.sets[setIndex].isDropset = isDropset; - if (drops != null) log.sets[setIndex].drops = drops; + final set = log.sets[setIndex]; + set.weight = weight; + set.reps = reps; + set.isDropset = isDropset; + set.drops = isDropset ? (drops ?? set.drops ?? []) : null; }); _markChanged(); },🤖 Prompt for 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. In `@workout-logger/lib/screens/edit_workout_session_screen.dart` around lines 390 - 395, The onSetChanged handler (the callback updating log.sets in EditWorkoutSessionScreen) fails to clear stale drop entries when isDropset flips false; update the handler inside setState so that when isDropset is false you explicitly set log.sets[setIndex].drops = null (or empty list per WorkoutSet.drops contract) in addition to updating weight/reps/isDropset, ensuring _save() will not serialize old drops for non-dropset rows.workout-logger/lib/screens/exercise_library_screen.dart (2)
201-225:
⚠️ Potential issue | 🟡 Minor | ⚡ Quick winClear action does not clear the visible search text.
Tapping the suffix icon resets
_query, but thisTextFieldis uncontrolled, so the old text stays rendered after the filter is cleared. That leaves the UI out of sync with the actual search state.🤖 Prompt for 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. In `@workout-logger/lib/screens/exercise_library_screen.dart` around lines 201 - 225, The TextField is uncontrolled, so clearing the suffix icon only updates _query but not the visible text; make the field controlled by creating a TextEditingController in the State (e.g., _searchController), pass controller: _searchController into the TextField, wire the controller to the existing onChanged callback (keep onChanged: onChanged), and in the suffix GestureDetector onTap call _searchController.clear() and then onChanged('') to keep UI and state in sync; also ensure you initialize the controller (optionally with initial text) and dispose it in dispose().
483-495:
⚠️ Potential issue | 🟠 Major | ⚡ Quick winDon't toggle selection when
selectionModeis off.This tap handler always mutates
_selectedIds, but the count row and submit CTA only exist whenselectionModeistrue. With the defaultfalse, the screen can enter a hidden selection state and gives the user no way to complete or clear it.🤖 Prompt for 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. In `@workout-logger/lib/screens/exercise_library_screen.dart` around lines 483 - 495, The tap handler for ExerciseCard is mutating _selectedIds regardless of selectionMode; change the onTap callback in the widget builder to first check selectionMode and only call setState and toggle _selectedIds when selectionMode is true (leave taps untouched when selectionMode is false). Update the code near the ExerciseCard creation where sel is computed from _selectedIds and the onTap lambda is defined so that the toggle logic is gated by the selectionMode boolean to avoid entering a hidden selection state.workout-logger/lib/screens/history_screen.dart (3)
236-260: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Use design system tokens for border radius.
The search toggle button uses hardcoded
borderRadius.circular(10)instead ofAppRadiustokens. As per coding guidelines, always use design system tokens rather than hardcoded values.♻️ Proposed fix
child: Container( width: 36, height: 36, decoration: BoxDecoration( color: _showSearch ? AppColors.primary.withValues(alpha: 0.15) : AppColors.glass2, - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(AppRadius.md), border: Border.all(📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.GestureDetector( onTap: () => setState(() { _showSearch = !_showSearch; if (!_showSearch) { _query = ''; _searchController.clear(); } }), child: Container( width: 36, height: 36, decoration: BoxDecoration( color: _showSearch ? AppColors.primary.withValues(alpha: 0.15) : AppColors.glass2, borderRadius: BorderRadius.circular(AppRadius.md), border: Border.all( color: _showSearch ? AppColors.primary.withValues(alpha: 0.4) : AppColors.glassBorder, ), ), child: Icon( _showSearch ? Icons.close_rounded : Icons.search_rounded, size: 16, color: _showSearch ? AppColors.primary : AppColors.textMuted, ), ), ),🤖 Prompt for 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. In `@workout-logger/lib/screens/history_screen.dart` around lines 236 - 260, The Container's decoration uses a hardcoded borderRadius.circular(10); replace that literal with the design token from AppRadius (e.g., use AppRadius.small) so the GestureDetector/Container decoration consistently uses the design system token (update BorderRadius.circular(10) to BorderRadius.circular(AppRadius.small) or equivalent AppRadius token and add any necessary import).Source: Coding guidelines
307-320: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Use design system tokens for border radius.
The calendar navigation buttons use hardcoded
borderRadius.circular(8)instead ofAppRadiustokens. As per coding guidelines, always use design system tokens rather than hardcoded values.♻️ Proposed fix
child: Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: AppColors.glass2, - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(AppRadius.sm), ),Apply to both left and right navigation buttons (lines 307-320 and 341-357).
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.GestureDetector( onTap: () => setState(() { _calendarMonth = DateTime(_calendarMonth.year, _calendarMonth.month - 1); _selectedDay = null; }), child: Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: AppColors.glass2, borderRadius: BorderRadius.circular(AppRadius.sm), ), child: const Icon(Icons.chevron_left_rounded, size: 18, color: AppColors.textMuted), ), ),🤖 Prompt for 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. In `@workout-logger/lib/screens/history_screen.dart` around lines 307 - 320, The calendar nav buttons use a hardcoded BorderRadius.circular(8); replace that with the design token (e.g., AppRadius.someRadius) in the Container decoration for the left and right navigation buttons so they use the design system. Locate the GestureDetector/Container widgets that update _calendarMonth and _selectedDay (the left chevron button with Icon(Icons.chevron_left_rounded) and the corresponding right chevron) and swap BorderRadius.circular(8) for the appropriate AppRadius token used elsewhere in the app.Source: Coding guidelines
434-455: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Use design system tokens for border radius.
The search bar uses hardcoded
borderRadius.circular(12)instead ofAppRadiustokens. As per coding guidelines, always use design system tokens rather than hardcoded values.♻️ Proposed fix
return Container( height: 44, decoration: BoxDecoration( color: AppColors.glass2, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(AppRadius.md), border: Border.all(color: AppColors.glassBorder), ),🤖 Prompt for 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. In `@workout-logger/lib/screens/history_screen.dart` around lines 434 - 455, The Container decoration uses a hardcoded borderRadius: BorderRadius.circular(12); replace this with the design token from your radius system (e.g. use AppRadius.<appropriateToken>) so the code becomes borderRadius: BorderRadius.circular(AppRadius.<token>) or BorderRadius.all(Radius.circular(AppRadius.<token>)); update the instance in history_screen.dart (the Container/TextField search bar decoration) to reference the AppRadius token instead of 12.Source: Coding guidelines
workout-logger/lib/screens/home_screen.dart (3)
278-306: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Use design system tokens for border radius.
The AI coach button uses hardcoded
borderRadius.circular(12)instead ofAppRadiustokens. As per coding guidelines, always use design system tokens rather than hardcoded values.♻️ Proposed fix
child: Container( width: 40, height: 40, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(AppRadius.md), gradient: const LinearGradient(📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.onTap: () => Navigator.push( context, MaterialPageRoute(builder: (_) => const AiCoachScreen()), ), child: Container( width: 40, height: 40, decoration: BoxDecoration( borderRadius: BorderRadius.circular(AppRadius.md), gradient: const LinearGradient( colors: [AppColors.primary, Color(0xFF5B21B6)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), boxShadow: [ BoxShadow( color: AppColors.primaryGlow(0.35), blurRadius: 12, spreadRadius: -3, ), ], ), child: const Icon( Icons.auto_awesome_rounded, size: 18, color: Colors.white, ), ), ),🤖 Prompt for 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. In `@workout-logger/lib/screens/home_screen.dart` around lines 278 - 306, Replace the hardcoded borderRadius.circular(12) in the AI coach button Container decoration with the design token from the app's radius tokens (use AppRadius.small instead of 12) so the snippet uses BorderRadius.circular(AppRadius.small) (update the Container/BoxDecoration in the HomeScreen build where borderRadius.circular(12) is used for the AI coach button that navigates to AiCoachScreen).Source: Coding guidelines
308-328: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Use design system tokens for border radius.
The profile button uses hardcoded
borderRadius.circular(12)instead ofAppRadiustokens. As per coding guidelines, always use design system tokens rather than hardcoded values.♻️ Proposed fix
child: Container( width: 40, height: 40, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(AppRadius.md), color: AppColors.glass2,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.GestureDetector( onTap: () => Navigator.push( context, MaterialPageRoute(builder: (_) => const ProfileScreen()), ), child: Container( width: 40, height: 40, decoration: BoxDecoration( borderRadius: BorderRadius.circular(AppRadius.md), color: AppColors.glass2, border: Border.all(color: AppColors.glassBorder), ), child: const Icon( Icons.person_outline_rounded, size: 18, color: AppColors.textSoft, ), ), ), ],🤖 Prompt for 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. In `@workout-logger/lib/screens/home_screen.dart` around lines 308 - 328, Replace the hardcoded BorderRadius.circular(12) used in the Container inside the GestureDetector (the profile button) with the design-system radius token from AppRadius (e.g., AppRadius.* appropriate token) so the widget uses the shared radius values; update the Container's decoration to use BorderRadius.circular(...) -> BorderRadius.all(AppRadius.<token>) or directly the token shape as your design system expects, ensuring the rest of the properties (color, border) remain unchanged and the ProfileScreen navigation and Icon usage are not modified.Source: Coding guidelines
464-507: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Use design system tokens for border radius.
The Start/Resume button uses hardcoded
borderRadius.circular(12)instead ofAppRadiustokens. As per coding guidelines, always use design system tokens rather than hardcoded values.♻️ Proposed fix
child: Container( padding: const EdgeInsets.symmetric( horizontal: 14, vertical: 10), decoration: BoxDecoration( color: isActive ? AppColors.warning : AppColors.primary, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(AppRadius.md), border: Border.all(📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.GestureDetector( onTap: isActive ? () => homeState?._resumeWorkout(context) : () => homeState?._showRoutineSelector(context), child: Container( padding: const EdgeInsets.symmetric( horizontal: 14, vertical: 10), decoration: BoxDecoration( color: isActive ? AppColors.warning : AppColors.primary, borderRadius: BorderRadius.circular(AppRadius.md), border: Border.all( color: Colors.white.withValues(alpha: 0.18), ), boxShadow: [ BoxShadow( color: (isActive ? AppColors.warning : AppColors.primary) .withValues(alpha: 0.35), blurRadius: 16, ), ], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( isActive ? Icons.play_arrow_rounded : Icons.flash_on_rounded, size: 12, color: Colors.white, ), const SizedBox(width: 4), Text( isActive ? 'Resume' : 'Start', style: GoogleFonts.geist( fontSize: 13, fontWeight: FontWeight.w600, color: Colors.white, ), ), ], ), ), ),🤖 Prompt for 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. In `@workout-logger/lib/screens/home_screen.dart` around lines 464 - 507, Replace the hardcoded BorderRadius.circular(12) in the Start/Resume button's Container decoration with the design system token (e.g., AppRadius.medium or the appropriate AppRadius constant) so the GestureDetector/Container uses the AppRadius token for borderRadius; update the BoxDecoration in the widget where borderRadius is set to reference AppRadius instead of BorderRadius.circular(12) and ensure imports for the design tokens are present if needed.Source: Coding guidelines
workout-logger/lib/screens/onboarding_screen.dart (1)
61-156: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Use spacing tokens consistently in the welcome form.
This block mixes
AppSpacing/AppRadiuswith raw layout literals (28,12,16,14), so the new screen can drift from the design system this PR is trying to centralize.Suggested direction
- padding: const EdgeInsets.symmetric(horizontal: 28), + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.lg), @@ - const SizedBox(height: 12), + const SizedBox(height: AppSpacing.sm), @@ - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 14, - ), + contentPadding: const EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: AppSpacing.sm, + ),As per coding guidelines, "Always use design system tokens (AppColors, AppSpacing, AppRadius) rather than hardcoded values".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.child: Padding( padding: const EdgeInsets.symmetric(horizontal: AppSpacing.lg), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Spacer(flex: 2), // Logo mark Container( width: 72, height: 72, decoration: BoxDecoration( borderRadius: BorderRadius.circular(AppRadius.lg), gradient: LinearGradient( colors: [ AppColors.primary, AppColors.secondary.withValues(alpha: 0.8), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), boxShadow: [ BoxShadow( color: AppColors.primary.withValues(alpha: 0.5), blurRadius: 32, spreadRadius: 4, ), ], ), child: const Icon( Icons.fitness_center_rounded, color: Colors.white, size: 36, ), ), const SizedBox(height: AppSpacing.xl), Text( 'Welcome to\nRepForge', style: GoogleFonts.geist( fontSize: 36, fontWeight: FontWeight.w800, color: AppColors.textPrimary, height: 1.1, letterSpacing: -1, ), ), const SizedBox(height: AppSpacing.sm), Text( 'Track every rep. Beat every record.\nForge your best self.', style: GoogleFonts.geist( fontSize: 15, color: AppColors.textMuted, height: 1.5, ), ), const Spacer(flex: 2), Text( 'WHAT SHOULD WE CALL YOU?', style: GoogleFonts.geist( fontSize: 11, fontWeight: FontWeight.w600, color: AppColors.textFaint, letterSpacing: 1.2, ), ), const SizedBox(height: AppSpacing.sm), TextField( controller: _controller, autofocus: true, textCapitalization: TextCapitalization.words, style: GoogleFonts.geist( color: AppColors.textPrimary, fontSize: 16, fontWeight: FontWeight.w500, ), decoration: InputDecoration( hintText: 'Your name', hintStyle: GoogleFonts.geist(color: AppColors.textFaint), filled: true, fillColor: AppColors.glass2, border: OutlineInputBorder( borderRadius: BorderRadius.circular(AppRadius.md), borderSide: BorderSide(color: AppColors.glassBorder), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppRadius.md), borderSide: BorderSide(color: AppColors.glassBorder), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppRadius.md), borderSide: BorderSide(color: AppColors.primary, width: 1.5), ), contentPadding: const EdgeInsets.symmetric( horizontal: AppSpacing.md, vertical: AppSpacing.sm, ),🤖 Prompt for 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. In `@workout-logger/lib/screens/onboarding_screen.dart` around lines 61 - 156, The UI uses hardcoded spacing and padding values (28, 12, 16, 14) inside the Column and TextField decorations instead of the design tokens; update those literals to use the appropriate tokens (e.g., replace horizontal padding 28 with AppSpacing.[sm/md/lg] or a consistent token, replace SizedBox height 12 with AppSpacing.[xs/sm], and replace contentPadding horizontal/vertical 16/14 with AppSpacing tokens) to match the design system and keep existing usage of AppRadius and AppSpacing elsewhere; ensure changes are applied around the Container/Padding/ SizedBox/TextField (referencing _controller, TextField, AppRadius, AppSpacing) so spacing is consistent across the onboarding_screen.dart.Source: Coding guidelines
workout-logger/lib/screens/profile_screen.dart (1)
205-207:
⚠️ Potential issue | 🟠 Major | ⚡ Quick winDon't report a canceled share as a successful export.
ShareResultStatus.dismissedmeans the user closed the share sheet without choosing a destination. Showing the success snackbar here can falsely reassure them that a backup was saved even though the file never left the app's temp directory.Suggested fix
- if (result.status == ShareResultStatus.success || - result.status == ShareResultStatus.dismissed) { + if (result.status == ShareResultStatus.success) { _showSnack('Backup exported successfully!', AppColors.success); + } else if (result.status == ShareResultStatus.dismissed) { + _showSnack('Backup export canceled.', AppColors.warning); }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.if (result.status == ShareResultStatus.success) { _showSnack('Backup exported successfully!', AppColors.success); } else if (result.status == ShareResultStatus.dismissed) { _showSnack('Backup export canceled.', AppColors.warning); }🤖 Prompt for 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. In `@workout-logger/lib/screens/profile_screen.dart` around lines 205 - 207, The code treats ShareResultStatus.dismissed as a successful export; change the condition so only ShareResultStatus.success triggers _showSnack('Backup exported successfully!', AppColors.success) and handle ShareResultStatus.dismissed separately (e.g., call _showSnack with a canceled/neutral message and AppColors.warning or do nothing) so users aren't misled that a backup left the app; update the check of result.status (and any surrounding export/share function) to only consider ShareResultStatus.success as success and add an else-if for ShareResultStatus.dismissed to show an appropriate cancellation message.workout-logger/lib/screens/routine_optimizer_screen.dart (1)
473-497:
⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd
constconstructor to_OptimizerAvatar.The
_OptimizerAvatarwidget at line 473 is missing aconstconstructor. As per coding guidelines, useconstconstructors wherever possible.🔧 Proposed fix
class _OptimizerAvatar extends StatelessWidget { + const _OptimizerAvatar({super.key}); + `@override` Widget build(BuildContext context) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.class _OptimizerAvatar extends StatelessWidget { const _OptimizerAvatar({super.key}); `@override` Widget build(BuildContext context) { return Container( width: 28, height: 28, decoration: BoxDecoration( gradient: const LinearGradient( colors: [AppColors.secondary, Color(0xFF0097A7)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(AppRadius.sm), boxShadow: [ BoxShadow( color: AppColors.secondaryGlow(0.35), blurRadius: 8, spreadRadius: -2, ), ], ), child: const Icon(Icons.auto_fix_high_rounded, color: Colors.white, size: 14), ); } }🤖 Prompt for 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. In `@workout-logger/lib/screens/routine_optimizer_screen.dart` around lines 473 - 497, The _OptimizerAvatar StatelessWidget lacks a const constructor; add a const constructor (const _OptimizerAvatar({Key? key}) : super(key: key);) to the _OptimizerAvatar class and update all places that instantiate _OptimizerAvatar to use const where applicable so the widget can be const-constructed and leverage compile-time optimizations.Source: Coding guidelines
workout-logger/lib/screens/routines_screen.dart (1)
494-518: 🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoff
Verify the routine session count threshold.
The code checks if
sessionCount < 3before allowing optimization (lines 497-510). This threshold appears arbitrary and might be too restrictive for users who want to optimize after 1-2 sessions. Consider if this threshold should be configurable or if a warning (rather than blocking) would be more appropriate.🤖 Prompt for 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. In `@workout-logger/lib/screens/routines_screen.dart` around lines 494 - 518, The current onTap handler in the GestureDetector blocks navigation to RoutineOptimizerScreen when sessionCount < 3; make the threshold configurable and/or change the behavior to warn instead of blocking: extract the hardcoded "3" into a configurable constant or a user setting (e.g., optimizerMinSessions) and reference it where sessionCount is computed, and if you prefer a soft warning, replace the early return with a SnackBar warning that still calls Navigator.push to RoutineOptimizerScreen(routine: routine) (or add an override confirmation dialog) so users can proceed after 1–2 sessions if they choose.workout-logger/lib/screens/widgets/activity_heatmap.dart (1)
23-41: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
Use design system tokens for spacing.
The
GridViewuses hardcoded spacing values (crossAxisSpacing: 3, mainAxisSpacing: 3) instead ofAppSpacingtokens. As per coding guidelines, always use design system tokens rather than hardcoded values.🤖 Prompt for 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. In `@workout-logger/lib/screens/widgets/activity_heatmap.dart` around lines 23 - 41, Replace the hardcoded spacing numbers in GridView.builder's SliverGridDelegateWithFixedCrossAxisCount with the design system tokens (use AppSpacing.small for crossAxisSpacing and mainAxisSpacing) so spacing comes from the design system; update the SliverGridDelegateWithFixedCrossAxisCount constructor in the activity_heatmap.dart GridView.builder (references: GridView.builder, SliverGridDelegateWithFixedCrossAxisCount, crossAxisSpacing, mainAxisSpacing) to use AppSpacing tokens instead of the literal 3.Source: Coding guidelines
workout-logger/lib/screens/widgets/analytics_overview.dart (1)
361-403: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Use design system tokens for border radius.
The range toggle component uses hardcoded border radius values (
10and7) instead ofAppRadiustokens. As per coding guidelines, always use design system tokens rather than hardcoded values.♻️ Proposed fix
return Container( padding: const EdgeInsets.all(3), decoration: BoxDecoration( color: AppColors.glass2, - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(AppRadius.md), border: Border.all(color: AppColors.glassBorder), ), child: Row( mainAxisSize: MainAxisSize.min, children: _Range.values.map((r) { final active = r == value; return GestureDetector( onTap: () => onChanged(r), child: AnimatedContainer( duration: const Duration(milliseconds: 180), curve: Curves.easeOut, padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: active ? AppColors.primary : Colors.transparent, - borderRadius: BorderRadius.circular(7), + borderRadius: BorderRadius.circular(AppRadius.sm),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.class _RangeToggle extends StatelessWidget { const _RangeToggle({required this.value, required this.onChanged}); final _Range value; final ValueChanged<_Range> onChanged; `@override` Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(3), decoration: BoxDecoration( color: AppColors.glass2, borderRadius: BorderRadius.circular(AppRadius.md), border: Border.all(color: AppColors.glassBorder), ), child: Row( mainAxisSize: MainAxisSize.min, children: _Range.values.map((r) { final active = r == value; return GestureDetector( onTap: () => onChanged(r), child: AnimatedContainer( duration: const Duration(milliseconds: 180), curve: Curves.easeOut, padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: active ? AppColors.primary : Colors.transparent, borderRadius: BorderRadius.circular(AppRadius.sm), ), child: Text( r.label, style: GoogleFonts.geistMono( fontSize: 11, fontWeight: FontWeight.w700, color: active ? Colors.white : AppColors.textMuted, ), ), ), ); }).toList(), ), ); } }🤖 Prompt for 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. In `@workout-logger/lib/screens/widgets/analytics_overview.dart` around lines 361 - 403, Replace the hardcoded BorderRadius.circular(10) and BorderRadius.circular(7) in the _RangeToggle widget with the design-system radius tokens (use the appropriate AppRadius token(s) from your design system, e.g. AppRadius.medium/AppRadius.small) so both the container decoration and the inner AnimatedContainer use AppRadius instead of numeric literals; update the BoxDecoration.borderRadius in _RangeToggle and the borderRadius on the inner AnimatedContainer to reference the AppRadius token(s).Source: Coding guidelines
workout-logger/lib/screens/widgets/calendar_grid.dart (2)
58-67: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
Use design system tokens for spacing.
The calendar grid uses hardcoded spacing values (
crossAxisSpacing: 4, mainAxisSpacing: 4) instead ofAppSpacingtokens. As per coding guidelines, always use design system tokens rather than hardcoded values.🤖 Prompt for 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. In `@workout-logger/lib/screens/widgets/calendar_grid.dart` around lines 58 - 67, The SliverGridDelegateWithFixedCrossAxisCount in GridView.builder currently uses hardcoded spacing (crossAxisSpacing: 4, mainAxisSpacing: 4); replace those literals with the design system tokens (e.g., AppSpacing.small or the appropriate AppSpacing constant) so both crossAxisSpacing and mainAxisSpacing reference AppSpacing instead of numeric values; update the GridView.builder/SliverGridDelegateWithFixedCrossAxisCount instantiation accordingly to use the token names.Source: Coding guidelines
188-203: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Use design system tokens for border radius.
The day cell uses hardcoded
borderRadius.circular(8)instead ofAppRadiustokens. As per coding guidelines, always use design system tokens rather than hardcoded values.♻️ Proposed fix
return GestureDetector( onTap: onTap, child: Container( decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(AppRadius.sm), color: bg, border: border,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.return GestureDetector( onTap: onTap, child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(AppRadius.sm), color: bg, border: border, boxShadow: isSelected ? [ BoxShadow( color: AppColors.primary.withValues(alpha: 0.25), blurRadius: 8, ), ] : null, ),🤖 Prompt for 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. In `@workout-logger/lib/screens/widgets/calendar_grid.dart` around lines 188 - 203, Replace the hardcoded borderRadius.circular(8) inside the Container's BoxDecoration (in the day cell widget returned by GestureDetector) with the design-system token (e.g., AppRadius.small) so it uses the AppRadius value instead of a magic number; update the borderRadius property to BorderRadius.circular(AppRadius.small) or the appropriate AppRadius token to match the design system.Source: Coding guidelines
workout-logger/lib/screens/widgets/editable_exercise_card.dart (2)
18-45: 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift
Make the editable models immutable and update them via
copyWith().These new model types are mutable state holders, and the edit flow now depends on mutating nested fields in place. That conflicts with the repo rule and makes future state updates much easier to miss once this UI starts relying on identity changes.
As per coding guidelines, "All model mutations must go through
copyWith()— never mutate state directly."🤖 Prompt for 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. In `@workout-logger/lib/screens/widgets/editable_exercise_card.dart` around lines 18 - 45, EditableExerciseLog and EditableSet are mutable but must be immutable and updated via copyWith; change all fields on EditableExerciseLog and EditableSet to final, make constructors constant/positional as appropriate, ensure lists (sets and drops) are stored as unmodifiable (e.g., List.unmodifiable) and then add copyWith methods on both classes (EditableExerciseLog.copyWith and EditableSet.copyWith) that let callers replace fields or produce modified copies of nested lists (e.g., return a new EditableExerciseLog with a modified sets list where an EditableSet is replaced via its copyWith, and EditableSet.copyWith should allow updating weight, reps, isDropset, drops, timeTaken, and timestamp). Update any code that previously mutated fields directly to use these copyWith helpers and to replace lists atomically rather than mutating in place.Source: Coding guidelines
331-352:
⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftDon't hardcode
kgin the workout editor rows.These inputs always label weights in kilograms, while the rest of this slice already formats weights through
SettingsProvider. In lb mode, this editor will still show raw stored kg values, which makes it easy to edit and save the wrong weight.Also applies to: 531-554
🤖 Prompt for 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. In `@workout-logger/lib/screens/widgets/editable_exercise_card.dart` around lines 331 - 352, The weight input is hardcoded to 'kg' in the _NumField (using _weightCtrl/_weightFocus and onWeightChanged), causing wrong display/edits in lb mode; update the suffix to read the unit label from SettingsProvider (or equivalent settings getter) and ensure displayed value and parsing honor the current unit by converting stored kg to the user unit when initializing _weightCtrl.text and converting back to kg before calling widget.onWeightChanged; apply the same change for the other occurrence around the 531-554 block so both editor rows use the settings-based unit and proper conversions.workout-logger/lib/screens/widgets/exercise_details_sheet.dart (1)
233-244:
⚠️ Potential issue | 🟡 Minor | ⚡ Quick winDon't hardcode a positive sign for the growth slope.
This section only checks
r2, not whethergrowthModel.slopeis positive. A negative trend will currently render as+-… volume/session, which is misleading.Suggested fix
- Text( - '+${settings.toDisplay(growthModel.slope).toStringAsFixed(1)} ${settings.unitLabel} volume/session', + Text( + '${settings.toDisplay(growthModel.slope) > 0 ? '+' : ''}${settings.toDisplay(growthModel.slope).toStringAsFixed(1)} ${settings.unitLabel} volume/session', style: const TextStyle( color: AppColors.success, fontSize: 13,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.child: Text( '${settings.toDisplay(growthModel.slope) > 0 ? '+' : ''}${settings.toDisplay(growthModel.slope).toStringAsFixed(1)} ${settings.unitLabel} volume/session', style: const TextStyle( color: AppColors.success, fontSize: 13, fontWeight: FontWeight.w500, ), ), ), Text( 'R² ${(growthModel.r2 * 100).toStringAsFixed(0)}%', style: const TextStyle(🤖 Prompt for 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. In `@workout-logger/lib/screens/widgets/exercise_details_sheet.dart` around lines 233 - 244, The Text currently hardcodes a '+' before the slope which causes "+-..." when growthModel.slope is negative; update the widget that builds the label (the Text containing '+${settings.toDisplay(growthModel.slope).toStringAsFixed(1)} ${settings.unitLabel} volume/session') to prepend '+' only when growthModel.slope > 0 (e.g., compute a sign = growthModel.slope > 0 ? '+' : '' and render "$sign${settings.toDisplay(growthModel.slope).toStringAsFixed(1)} ${settings.unitLabel} volume/session"), keeping the existing settings.toDisplay and unitLabel usage so negative values retain their '-' sign.workout-logger/lib/screens/widgets/exercise_input_section.dart (1)
877-892: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Replace the hardcoded deload color with an
AppColorstoken.This is the one state in the new banner that still bypasses the centralized theme palette, so it will drift from the color refactor in this PR.
Suggested fix
border: Border.all( color: week.isDeload - ? Colors.amber.withValues(alpha: 0.4) + ? AppColors.warning.withValues(alpha: 0.4) : AppColors.primary.withValues(alpha: 0.3), ), @@ child: Icon(Icons.battery_charging_full_rounded, - size: 14, color: Colors.amber), + size: 14, color: AppColors.warning), ),As per coding guidelines, "Always use design system tokens (AppColors, AppSpacing, AppRadius) rather than hardcoded values."
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.border: Border.all( color: week.isDeload ? AppColors.warning.withValues(alpha: 0.4) : AppColors.primary.withValues(alpha: 0.3), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ if (week.isDeload) const Padding( padding: EdgeInsets.only(right: 4), child: Icon(Icons.battery_charging_full_rounded, size: 14, color: AppColors.warning),🤖 Prompt for 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. In `@workout-logger/lib/screens/widgets/exercise_input_section.dart` around lines 877 - 892, The deload banner is using hardcoded Colors.amber in two places; replace those with the design token from AppColors (e.g., AppColors.amber or the project's deload/warning token) so the theme system is used consistently. Update the Border.all color branch (where it currently uses week.isDeload ? Colors.amber.withValues(alpha: 0.4) ...) and the Icon color (Icon(..., color: Colors.amber)) to use the matching AppColors token and its withValues(alpha: ...) variant; keep the week.isDeload condition and existing sizing/padding intact.Source: Coding guidelines
…tection Replaces the single exponentially-weighted linear regression with a two-candidate fit chosen by weighted residual error: - Linear and logarithmic (y = a + b·ln(1+x)) candidates — the log curve captures the diminishing returns muscle growth actually follows instead of promising linear gains forever; only eligible with >=6 points over >=14 days and a 2% RSS margin to prevent flip-flopping - One Tukey-bisquare robust re-weighting pass per candidate so a single deload or cut-short session no longer tilts the trend - GrowthModel now carries curve type, coefficient, lastX and a weighted residual stdError; slope is the instantaneous per-day rate at the newest point, so all existing consumers stay semantically correct - New weeklyGrowthPercent (growth relative to current level) drives scale-independent plateau/decline detection: same thresholds work for a novice bench and a 10t weekly squat volume - recommendSets adds a deload branch (~10% back-off, plate-rounded) when volume is genuinely regressing; trend signals require r2 > 0.2 - predictTargetCompletion inverts the fitted curve (log-aware), caps predictions at 2 years; confidence interval now derived from stdError converted to days instead of an ad-hoc R² heuristic - Volume chart trend line now evaluates the model at day offsets instead of session indices (pre-existing mismatch); per-muscle trend arrows use relative weekly growth; mislabeled kg/session displays fixed to kg/week - AI coach growth payload exposes curve, weekly_growth_percent and a plateauing/declining/improving trend https://claude.ai/code/session_01FTgsHTfbvXsxwTUe74UrYe
Covers SleepHrSnapshot data model, 10-min bar chart with stage colors, moving-average trend line, and the per-stage HR distribution (range bar) chart replacing the two REM/Deep comparison cards. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Added DebugLogBuffer class to capture and store debugPrint calls in a circular buffer. - Integrated DebugLogBuffer into the main application via the attach method. refactor: Update GeminiContextBuilder instructions and workflow - Revised instructions for modifying user data and clarified the workflow steps. - Enhanced clarity on data fetching and analysis processes. fix: Improve HealthConnectService error handling and logging - Added detailed debug prints for HealthConnectService methods. - Adjusted permissions handling for heart rate data to use heartRateSeries. - Enhanced error handling for read permissions and data fetching. feat: Enhance ReadinessManager with sleep HR snapshot functionality - Integrated SleepHrSnapshot to track heart rate during sleep. - Improved refresh logic to build and display sleep HR data. - Added debug tracing for readiness calculations and data fetching. refactor: Update ReadinessCalculator for accurate sleep duration calculations - Modified lastNightSleep method to return total sleep minutes instead of the longest period. - Maintained backward compatibility with a synthetic SleepPeriod return.
…nagement - Added HealthHistoryManager to handle sleep and heart rate data aggregation and caching. - Introduced utility functions for date range calculations and stepping through time periods. - Implemented methods to fetch and cache daily heart rate snapshots and aggregated sleep data. - Created a new sleep_hr_builder utility for building sleep HR snapshots for any night. - Updated ReadinessManager to utilize the new HealthHistoryManager for daily HR snapshots. - Added unit tests for HealthHistoryManager covering various scenarios for sleep and heart rate data.
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 34035904 | Triggered | Generic Password | 57ee362 | workout-logger/android/app/build.gradle.kts | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secret safely. Learn here the best practices.
- Revoke and rotate this secret.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
* feat: add workout summary screen and enhance app theme colors
- Implemented WorkoutSummaryScreen to display post-workout details including duration, volume, sets, and exercises.
- Added visual elements such as trophy header and muscle groups trained section.
- Refactored AppTheme to centralize color management through AppColors, improving maintainability and consistency across the app.
- Updated theme properties for better visual coherence and modern styling.
* feat: add predictive back page transitions for Android in AppTheme
* Refactor Programs Screen and add Week Structure Editor
- Updated ProgramsScreen to improve UI elements and replace AppTheme with AppColors.
- Enhanced FloatingActionButton styles and added new RFWidgets for better consistency.
- Implemented a new ProgramWeekEditorStep widget for editing week structures in training programs.
- Introduced ProgramWeekTile for displaying collapsible week cards in ProgramDetailScreen.
- Added functionality for managing deload weeks and intensity factors within the week editor.
- Improved overall code organization and readability across the modified files.
* feat: Enhance UI with new glassmorphic components and charts
- Introduced `GlassCard` with gradient background and accent border option.
- Added `AmbientGlow` for decorative ambient effects.
- Implemented `RFNavBar` for a custom bottom navigation bar with glassmorphism.
- Created `Sparkline` and `VolumeChart` widgets for data visualization.
- Developed `WheelPickerField` for weight and reps input with haptic feedback.
- Updated `WorkoutHeader` with gradient background and improved text styles.
- Refined `AppTheme` with new color definitions and text styles using Google Fonts.
- Added Google Fonts dependency for enhanced typography.
* feat: add personal records feature
- Introduced PersonalRecord model to track best weight, reps, and volume for exercises.
- Implemented PRManager to manage personal records, including checking and updating records after workouts.
- Added methods in storage service for saving and retrieving personal records.
- Updated UI to display personal records in the Analytics screen and Workout Summary screen.
- Enhanced onboarding process to prompt for user name and handle version updates.
- Refactored various screens to improve layout and user experience.
* feat: update exercise library screen tests with new icon and text changes
* feat: update .gitignore and CLAUDE.md for new graphify output and project guidelines; remove MainActivity.kt
* analytics_screen.dart
_FrequencyGrid: added w >= 0 && guard so future-dated sessions can't insert negative map keys
_MuscleVolumeChart: early-return isEmpty when maxVol == 0 (prevents NaN); converted volume display via settings.toDisplay + settings.unitLabel
dashboard_widgets.dart
startOfWeek now truncated to midnight (DateTime(y,m,d)) before subtracting weekdays — fixes the bug where sessions earlier in the day than "now" were excluded
Volume StatGridCard now uses settings.toDisplay(weeklyVolume) and 'Volume (${settings.unitLabel})'
edit_workout_session_screen.dart
drops: s.drops?.toList() — defensive copy prevents shared-mutation with the original WorkoutSession
exercise_details_sheet.dart
Added else branch on delete failure — shows error SnackBar instead of silently doing nothing
Set chips now use settings.toDisplay(s.weight) + settings.unitLabel
Growth trend label uses settings.toDisplay(growthModel.slope) + settings.unitLabel
exercise_input_section.dart
Icon(Icons.auto_awesome_rounded, …) → const Icon(…)
exercise_progress_view.dart
DropdownButton.value guarded with ids.contains(selected) ? selected : null — prevents assert/crash when the selected exercise id is no longer in the performed set
profile_sections.dart
const _SectionDivider() and const _ComingSoonBadge() constructors + all call sites updated
rf_inputs.dart
Added onLongPressEnd to _StepButton (wired to GestureDetector.onLongPressEnd)
_NumberPickerSheetState now holds Timer? _holdTimer; onLongPress assigns _holdTimer = Timer.periodic(…), onLongPressEnd cancels it, dispose() also cancels it — no more leaked timers
session_details_sheet.dart
Volume banner: settings.toDisplay(session.totalVolume) + 'Volume ${settings.unitLabel}'
Per-exercise total: settings.toDisplay(log.totalVolume) + settings.unitLabel
Per-set row: settings.toDisplay(set.weight) formatted + settings.unitLabel
targets_tab.dart
Added _isSubmitting bool; _submit guards against re-entry and wraps provider call in try/finally; GlowButton.onPressed is null while submitting
workout_flow_screen.dart
_toggleDropset: weight controller text now uses settings.toDisplay(_currentWeight) with proper decimal formatting (matching _loadLastSessionData)
_addDrop: new drop controller text also uses settings.toDisplay(newWeight)
WorkoutHeader now receives restSeconds: _restSeconds
workout_header.dart
Added optional restSeconds field (defaults to 90 for backwards compatibility); _OptionsMenu receives widget.restSeconds instead of the hardcoded literal
workout_summary_screen.dart
volStr built from settings.toDisplay(session.totalVolume); label updated to 'Volume (${settings.unitLabel})'
* feat: enhance UI components and improve data handling across screens
* feat: update ExerciseLibraryScreen tests to include SettingsProvider in widget setup
* feat: enhance workout session data collection by including weight in HealthConnectService
* feat: update Android build configuration, enhance analytics screen, and improve UI components
* feat: add muscle recovery and growth tracking features in WorkoutProvider and MLService
* feat: refactor NumberInputCard to StatefulWidget and enhance RFNavBar design
* feat: enhance ProfileScreen and ProfileSections with Google Fonts for improved typography
* feat: update Create button in ProgramsScreen to allow for non-full width display
* feat: Integrate Gemini AI features for personalized coaching and insights
- Added GeminiService for AI integration, including chat and program generation capabilities.
- Implemented GeminiContextBuilder to create context strings for AI prompts.
- Introduced _WeeklyInsightsCard to display weekly insights based on user workouts.
- Added AiSettingsSection in profile settings for managing Gemini API key and model selection.
- Updated home screen to include weekly insights and AI coach button.
- Enhanced ProgramsScreen with AI program generator button.
- Updated SettingsProvider to manage Gemini API key and insights storage.
- Added necessary dependencies for Google Generative AI.
* feat: enhance various screens and models with improved error handling, UI adjustments, and new features
* feat: improve error handling in program saving and adjust opacity calculation in body heatmap
* Add comprehensive tests for MLService, model serialization, PRManager, and WorkoutProvider
- Implement tests for MLService covering growth model training, set recommendations, default recommendations, target completion prediction, and muscle recovery score computation.
- Create model serialization tests for WorkoutSet, ExerciseLog, WorkoutSession, Exercise, MuscleGroup, Target, GrowthModel, Routine, PersonalRecord, TrainingProgram, and ProgramExerciseSlot to ensure data integrity during JSON serialization/deserialization.
- Introduce tests for PRManager to validate loading records, backfilling from sessions, checking and updating personal records, and retrieving records.
- Enhance WorkoutProvider tests to verify initialization, session and routine loading, active workout management, and exercise name retrieval.
* fix: ensure opacity calculation in heatmap drawing is explicitly a double
* chore: update version to 2.0.0+21 in pubspec.yaml
* feat: enhance UI responsiveness with layout adjustments and breakpoints across multiple screens
* feat: enhance UI layout and responsiveness with padding adjustments for floating action buttons and improved data representation
* feat: add advanced metrics toggle and display estimated 1RM badge in workout input section
* Add widget tests for AnalyticsScreen and ExerciseProgressView
- Implement comprehensive widget tests for the AnalyticsScreen, covering the Overview, Targets, and Records tabs.
- Validate functionality such as tab switching, data display, and interaction with UI elements.
- Add widget tests for ExerciseProgressView, including exercise picker, chart mode toggle, and set progression chart.
- Ensure tests cover empty states, search functionality, and interaction with various UI components.
* feat: update app label handling and improve navigation in home screen
* feat: Implement Gemini AI service for AI coach functionality (#49)
* feat: Implement Gemini AI service for AI coach functionality
- Added GeminiAiService to handle AI coach chat, program generation, and insights using Google Generative AI.
- Created IAiService interface to define the contract for AI services.
- Developed GeminiContextBuilder to construct context for AI interactions.
- Introduced ConversationManager to manage AI conversations, including persistence and active conversation logic.
- Implemented storage service methods for saving and retrieving AI conversations.
- Built AiCoachViewModel to orchestrate AI interactions and manage conversation state.
- Added unit tests for AiCoachViewModel, CoachToolService, and ConversationManager to ensure functionality and persistence.
- Updated analytics and exercise progress views to use the new GeminiAiService.
* feat: Enhance Gemini AI service with token usage tracking and UI updates for AI coach
* feat: Refactor AiCoachViewModel and related services for improved readability and error handling
* feat: Improve error handling and async behavior in Gemini AI service usage tracking
---------
Co-authored-by: Devasy Patel <110348311+Devasy23@users.noreply.github.com>
* feat: Add muscle volume trend chart and recent sessions section in MuscleDetailSheet
* docs: add conversational routine optimizer design spec
Replaces the one-shot optimizer sheet with a dedicated conversational
optimizer screen that reuses the coach streaming tool-loop, adds an
interactive ask_user_questions tool, gates on insufficient data, and
persists sessions to a separate optimizer inbox.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor: remove one-shot routine optimizer (replaced by conversational flow)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add Conversation.kind and AI question models (QuestionSpec, AnswerSpec, PendingQuestions)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: scope ConversationManager by kind ('coach' | 'optimizer')
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add ask_user_questions tool declaration and optimizer system prompt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add RFQuestionCard reusable widget (option chips + custom input)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: rewrite RoutineOptimizerViewModel as conversational streaming VM with ask_user_questions pausing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: handle aborted completer and await appendMessage in submitAnswers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add RoutineOptimizerScreen (conversational UI with question card + history inbox)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* style: fix magic padding values and header button consistency in RoutineOptimizerScreen
* feat: add data gate (<3 sessions) and navigate to RoutineOptimizerScreen from routine card
* feat: add testBody method for RoutineOptimizerScreen to facilitate widget testing
* feat: add workout summary screen and enhance app theme colors (#50)
feat: add workout summary screen, health analytics, and production APK signing
- Workout summary screen with session details
- Sleep HR chart and debug log buffer
- HealthHistoryManager for sleep/heart rate data
- Workout heart rate analysis and recovery metrics
- EC P-256 production keystore signing via GitHub Secrets
- Split-per-ABI APK builds for arm64, armeabi-v7a, x86_64
- FUTURE_IMPROVEMENTS.md documenting Options B & C (F-Droid, fastlane)
* test: update lastNightSleep test to sum all periods in the night window
* feat: bundle Geist fonts locally and add F-Droid metadata (Option B) (#51)
- Remove google_fonts dependency; replace with bundled variable font files
(Geist-Variable.ttf + GeistMono-Variable.ttf from Vercel v1.7.2, MIT licensed)
- Replace all 377 GoogleFonts.geist/geistMono() calls with TextStyle(fontFamily:)
across 31 dart files — no runtime Google CDN fetch, F-Droid build-compatible
- Declare fonts in pubspec.yaml flutter.fonts section
- Add fdroid/metadata/com.devasy.repforge.yml with anti-features (NonFreeNet)
and auto-update config for F-Droid submission
Co-authored-by: Devasy Patel <110348311+Devasy23@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Devasy Patel <110348311+Devasy23@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Summary by CodeRabbit
Release Notes
New Features
Improvements
Bug Fixes & Updates