Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/test.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,4 +46,10 @@ jobs:

- name: Run tests
working-directory: ./workout-logger
run: flutter test
run: flutter test --coverage

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
files: workout-logger/coverage/lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
Comment on lines +51 to +55

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Consider adding fail_ci_if_error: true to surface upload failures.

Without this, a failed upload (bad token, network error) is silently swallowed and the step still shows green. The official Codecov action example explicitly includes fail_ci_if_error: true for exactly this reason.

⚙️ Proposed addition
 - name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
files: workout-logger/coverage/lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
+ fail_ci_if_error: true
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/test.yml around lines 51 - 55, Update the Codecov upload
step ("Upload coverage to Codecov") that uses codecov/codecov-action@v5 to
include the fail_ci_if_error: true input so upload failures (bad token, network
errors) cause the job to fail; modify the step definition where files and token
are set to add fail_ci_if_error: true alongside those inputs.

4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -77,8 +77,8 @@ flutter test test/workout_provider_test.dart
# Check lint / static analysis
flutter analyze

# Build release APK
flutter build apk --release
# Build release APKs (split per ABI)
flutter build apk --release --split-per-abi --obfuscate --split-debug-info=build/debug-info

# Generate Mockito mocks (after modifying interfaces)
dart run build_runner build --delete-conflicting-outputs
Expand Down
6 changes: 5 additions & 1 deletion workout-logger/android/app/build.gradle.kts
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,7 +24,11 @@ android {
applicationId = "com.devasy.repforge"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
// MIGRATION NOTE: minSdk is intentionally set to 26 (Android 8.0 Oreo).
// Health Connect requires API 26+. Devices running API <26 are no longer
// supported. If downgrading, remove the health_connector dependency and
// all HealthConnectService usages, then restore minSdk to flutter.minSdkVersion.
minSdk = 26
Comment thread
Devasy marked this conversation as resolved.
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
Expand Down
23 changes: 23 additions & 0 deletions workout-logger/android/app/src/main/AndroidManifest.xml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.health.WRITE_EXERCISE" />
<uses-permission android:name="android.permission.health.READ_EXERCISE" />
Comment thread
Devasy marked this conversation as resolved.

<application
android:label="RepForge"
android:name="${applicationName}"
Expand All@@ -24,7 +27,22 @@
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<!-- Required for Health Connect to show permissions dialog and list app in HC settings -->
<intent-filter>
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
</intent-filter>
</activity>
<!-- Required on Android 14+ so Health Connect can open your app's privacy rationale -->
<activity-alias
android:name="ViewPermissionUsageActivity"
android:exported="true"
android:targetActivity=".MainActivity"
android:permission="android.permission.START_VIEW_PERMISSION_USAGE">
<intent-filter>
<action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
<category android:name="android.intent.category.HEALTH_PERMISSIONS" />
</intent-filter>
</activity-alias>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
Expand All@@ -41,5 +59,10 @@
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
<!-- Health Connect availability check -->
<package android:name="com.google.android.apps.healthdata" />
<intent>
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
</intent>
</queries>
</manifest>
21 changes: 21 additions & 0 deletions workout-logger/lib/main.dart
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,12 +9,16 @@ import 'package:provider/provider.dart';

import 'services/storage_service.dart';
import 'services/ml_service.dart';
import 'services/health_connect_service.dart';
import 'services/interfaces/storage_service_interface.dart';
import 'services/interfaces/ml_service_interface.dart';
import 'services/interfaces/health_connect_service_interface.dart';
import 'services/workout_provider.dart';
import 'services/settings_provider.dart';
import 'services/api_service.dart';
import 'services/managers/program_manager.dart';
import 'services/managers/history_manager.dart';
import 'services/managers/health_sync_manager.dart';
import 'theme/app_theme.dart';
import 'screens/home_screen.dart';

Expand DownExpand Up@@ -45,8 +49,15 @@ class WorkoutLoggerApp extends StatelessWidget {
// This ensures the same instances are used throughout the app lifecycle
static final IStorageService _storageService = StorageService();
static final IMLService _mlService = MLService();
static final IHealthConnectService _healthConnectService = HealthConnectService();
static final ProgramManager _programManager = ProgramManager(_storageService);
static final SettingsProvider _settingsProvider = SettingsProvider(_storageService);
// HealthSyncManager uses the in-memory settings flag — no storage I/O on sync.
static final HealthSyncManager _healthSyncManager =
HealthSyncManager(_healthConnectService, _settingsProvider);
// HistoryManager is the single owner of session history + HC sync trigger.
static final HistoryManager _historyManager =
HistoryManager(_storageService, healthSyncManager: _healthSyncManager);

const WorkoutLoggerApp({super.key});

Expand All@@ -61,17 +72,23 @@ class WorkoutLoggerApp extends StatelessWidget {
Provider<IStorageService>.value(value: _storageService),
// Provide the ML service interface for direct access if needed
Provider<IMLService>.value(value: _mlService),
// IHealthConnectService stays in tree for ProfileScreen permission flow
Provider<IHealthConnectService>.value(value: _healthConnectService),
// Provide the ApiService singleton via DI
Provider<ApiService>.value(value: ApiService()),
// ProgramManager passed to tree directly
ChangeNotifierProvider<ProgramManager>.value(value: _programManager),
// SettingsProvider for user preferences (weight unit, increments)
ChangeNotifierProvider<SettingsProvider>.value(value: _settingsProvider),
// HistoryManager is the single source of truth for session history.
// Provided as ChangeNotifier so HistoryScreen rebuilds on sync badge changes.
ChangeNotifierProvider<HistoryManager>.value(value: _historyManager),
// WorkoutProvider receives dependencies via constructor injection
ChangeNotifierProvider(
create: (_) => WorkoutProvider(
_storageService,
mlService: _mlService,
historyManager: _historyManager,
programManager: _programManager,
),
),
Expand DownExpand Up@@ -111,6 +128,10 @@ class _AppInitializerState extends State<AppInitializer> {
final settings = context.read<SettingsProvider>();
await settings.init();

// Load HistoryManager session list (independent of WorkoutProvider).
final historyManager = context.read<HistoryManager>();
await historyManager.loadSessions();

// Fire-and-forget analytics in background
final api = context.read<ApiService>();
api.sendHeartbeat();
Expand Down
11 changes: 11 additions & 0 deletions workout-logger/lib/models/models.dart
Original file line numberDiff line numberDiff line change
Expand Up@@ -225,6 +225,8 @@ class WorkoutSession {
final List<ExerciseLog> exercises;
final int duration; // minutes
final String? notes;
/// Non-null when this session was successfully synced to Health Connect.
final DateTime? hcSyncedAt;

WorkoutSession({
required this.id,
Expand All@@ -233,6 +235,7 @@ class WorkoutSession {
required this.exercises,
required this.duration,
this.notes,
this.hcSyncedAt,
});

double get totalVolume =>
Expand All@@ -245,6 +248,7 @@ class WorkoutSession {
'exercises': exercises.map((e) => e.toJson()).toList(),
'duration': duration,
'notes': notes,
'hcSyncedAt': hcSyncedAt?.toIso8601String(),
};

factory WorkoutSession.fromJson(Map<String, dynamic> json) => WorkoutSession(
Expand All@@ -256,6 +260,9 @@ class WorkoutSession {
.toList(),
duration: json['duration'],
notes: json['notes'],
hcSyncedAt: json['hcSyncedAt'] != null
? DateTime.parse(json['hcSyncedAt'] as String)
: null,
);

WorkoutSession copyWith({
Expand All@@ -265,6 +272,7 @@ class WorkoutSession {
Object? exercises = _sentinel,
Object? duration = _sentinel,
Object? notes = _sentinel,
Object? hcSyncedAt = _sentinel,
}) => WorkoutSession(
id: id == _sentinel ? this.id : id as String,
date: date == _sentinel ? this.date : date as DateTime,
Expand All@@ -274,6 +282,9 @@ class WorkoutSession {
: exercises as List<ExerciseLog>,
duration: duration == _sentinel ? this.duration : duration as int,
notes: notes == _sentinel ? this.notes : notes as String?,
hcSyncedAt: hcSyncedAt == _sentinel
? this.hcSyncedAt
: hcSyncedAt as DateTime?,
);
}

Expand Down
Loading
Loading