diff --git a/.continue/mcpServers/new-mcp-server.yaml b/.continue/mcpServers/new-mcp-server.yaml new file mode 100644 index 0000000..0e32aa6 --- /dev/null +++ b/.continue/mcpServers/new-mcp-server.yaml @@ -0,0 +1,10 @@ +name: New MCP server +version: 0.0.1 +schema: v1 +mcpServers: + - name: New MCP server + command: npx + args: + - -y + - + env: {} diff --git a/lib/database/database.dart b/lib/database/database.dart index f6e5649..2e5c343 100644 --- a/lib/database/database.dart +++ b/lib/database/database.dart @@ -151,6 +151,11 @@ class AppDatabase extends _$AppDatabase { Future endRide(int rideId, RidesCompanion data) async { await (update(rides)..where((t) => t.id.equals(rideId))).write(data.copyWith(endTime: Value(DateTime.now()))); } + + /// Persists a completed performance run. Called by [PerformanceTimerService]. + Future addPerformanceRun(PerformanceRunsCompanion entry) { + return into(performanceRuns).insert(entry); + } } LazyDatabase _openConnection() { diff --git a/lib/features/settings/field_test_report_screen.dart b/lib/features/settings/field_test_report_screen.dart index 88ee854..b855583 100644 --- a/lib/features/settings/field_test_report_screen.dart +++ b/lib/features/settings/field_test_report_screen.dart @@ -190,13 +190,13 @@ class _FieldTestReportScreenState extends ConsumerState { final telemetryIntegrity = s != null ? s.validPercentage.toStringAsFixed(1) : '100.0'; final gpsIntegrity = s != null && s.totalSamples > 0 - ? ((s.gpsFixCount / s.totalSamples) * 100.0).clamp(0.0, 100.0).toStringAsFixed(1) + ? (((s.gpsFixCount ?? 0) / s.totalSamples) * 100.0).clamp(0.0, 100.0).toStringAsFixed(1) : '100.0'; final sensorIntegrity = s != null && s.totalSamples > 0 - ? (100.0 - (s.imuSaturationCount / s.totalSamples * 100.0)).clamp(0.0, 100.0).toStringAsFixed(1) + ? (100.0 - ((s.imuSaturationCount ?? 0) / s.totalSamples * 100.0)).clamp(0.0, 100.0).toStringAsFixed(1) : '100.0'; final dbIntegrity = s != null && s.totalSamples > 0 - ? (100.0 - (s.databaseSamplesLost / s.totalSamples * 100.0)).clamp(0.0, 100.0).toStringAsFixed(1) + ? (100.0 - ((s.databaseSamplesLost ?? 0) / s.totalSamples * 100.0)).clamp(0.0, 100.0).toStringAsFixed(1) : '100.0'; return Card( diff --git a/lib/services/field_test/field_test_logger.dart b/lib/services/field_test/field_test_logger.dart index 0f547e3..6cf2e8c 100644 --- a/lib/services/field_test/field_test_logger.dart +++ b/lib/services/field_test/field_test_logger.dart @@ -804,7 +804,7 @@ class FieldTestLogger { databaseSamplesLost: drift.Value(_droppedEventsCount), eventCount: drift.Value(events.length), overallConfidence: drift.Value(qualityReport.overallScore / 100.0), - rideQualityScore: drift.Value(qualityReport.overallScore), + rideQualityScore: drift.Value(qualityReport.overallScore.toDouble()), rideQualityRating: drift.Value(qualityReport.rating), wasRecoveredAfterCrash: drift.Value(wasRecovered), wasInterrupted: drift.Value(ride?.endTime == null || wasRecovered), diff --git a/lib/services/github/github_sync_service.dart b/lib/services/github/github_sync_service.dart index 03fdcb9..8d50c16 100644 --- a/lib/services/github/github_sync_service.dart +++ b/lib/services/github/github_sync_service.dart @@ -9,6 +9,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../../core/version/app_identity.dart'; import '../../database/database.dart'; import '../../features/records/records_service.dart'; +import '../auth/auth_providers.dart'; import '../auth/auth_service.dart'; import '../export_service.dart'; import '../field_test/field_test_export_service.dart'; @@ -75,12 +76,15 @@ class GitHubSyncService extends ChangeNotifier { bool isRideUploaded(int rideId) { final key = _userScopedKey('motorcloud_uploaded_ride', rideId); - return key != null && (_prefs.getBool(key) ?? false); + if (key != null) return _prefs.getBool(key) ?? false; + // Legacy unscoped key – used when no authenticated user is available. + return _prefs.getBool('motorcloud_uploaded_ride_$rideId') ?? false; } String? getRideCommitSha(int rideId) { final key = _userScopedKey('motorcloud_ride_remote_id', rideId); - return key == null ? null : _prefs.getString(key); + if (key != null) return _prefs.getString(key); + return _prefs.getString('motorcloud_ride_remote_id_$rideId'); } String? getRideUploadTimestamp(int rideId) { diff --git a/lib/services/sync/cloud_reconciliation_service.dart b/lib/services/sync/cloud_reconciliation_service.dart index dbfd8bf..2089d2e 100644 --- a/lib/services/sync/cloud_reconciliation_service.dart +++ b/lib/services/sync/cloud_reconciliation_service.dart @@ -1,4 +1,5 @@ import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:drift/drift.dart' show Variable; import '../../database/database.dart'; import '../auth/auth_service.dart'; @@ -34,12 +35,10 @@ class CloudReconciliationService { final FirebaseFirestore _firestore; CloudReconciliationService({ - required AppDatabase db, - required AuthService authService, + required AppDatabase this._db, + required AuthService this._authService, FirebaseFirestore? firestore, - }) : _db = db, - _authService = authService, - _firestore = firestore ?? FirebaseFirestore.instance; + }) : _firestore = firestore ?? FirebaseFirestore.instance; Future reconcileMetadata() async { final user = _authService.currentUser; @@ -194,8 +193,8 @@ class CloudReconciliationService { // Only flag meaningful numeric divergence. Missing cloud fields are not // treated as authoritative and therefore cannot overwrite local values. - if (cloudDistance != null && (localDistance - cloudDistance).abs() > 0.0001) return true; - if (cloudTopSpeed != null && (localTopSpeed - cloudTopSpeed).abs() > 0.0001) return true; + if (cloudDistance != null && localDistance != null && (localDistance - cloudDistance).abs() > 0.0001) return true; + if (cloudTopSpeed != null && localTopSpeed != null && (localTopSpeed - cloudTopSpeed).abs() > 0.0001) return true; return false; } diff --git a/lib/services/sync/firebase_sync_provider.dart b/lib/services/sync/firebase_sync_provider.dart index 0447213..6db304f 100644 --- a/lib/services/sync/firebase_sync_provider.dart +++ b/lib/services/sync/firebase_sync_provider.dart @@ -16,9 +16,8 @@ class FirebaseSyncProvider implements SyncProvider { final R2UploadService _r2UploadService; final SyncBundleWorker _bundleWorker; - FirebaseSyncProvider({required AppDatabase db, required AuthService authService, FirebaseFirestore? firestore, R2UploadService? r2UploadService, SyncBundleWorker? bundleWorker}) + FirebaseSyncProvider({required AppDatabase db, required AuthService this._authService, FirebaseFirestore? firestore, R2UploadService? r2UploadService, SyncBundleWorker? bundleWorker}) : _db = db, - _authService = authService, _firestore = firestore ?? _resolveFirestore(), _r2UploadService = r2UploadService ?? R2UploadService(), _bundleWorker = bundleWorker ?? SyncBundleWorker(db); diff --git a/lib/services/sync/r2_download_service.dart b/lib/services/sync/r2_download_service.dart index 577bd8f..b75231d 100644 --- a/lib/services/sync/r2_download_service.dart +++ b/lib/services/sync/r2_download_service.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:typed_data'; import 'package:crypto/crypto.dart'; import 'package:flutter/foundation.dart'; diff --git a/lib/services/sync/sync_manager.dart b/lib/services/sync/sync_manager.dart index 70f1f6a..b64fc29 100644 --- a/lib/services/sync/sync_manager.dart +++ b/lib/services/sync/sync_manager.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'sync_models.dart'; import 'sync_outbox.dart'; @@ -19,7 +18,7 @@ class SyncManager extends ChangeNotifier with WidgetsBindingObserver { SyncManager(this._outbox, {List? initialProviders, String? defaultProviderId, DateTime? Function()? nowProvider}) : _nowProvider = nowProvider ?? (() => DateTime.now()) { if (initialProviders != null) { - for (final p in initialProviders) registerProvider(p); + for (final p in initialProviders) { registerProvider(p); } } if (defaultProviderId != null) _activeProviderId = defaultProviderId; WidgetsBinding.instance.addObserver(this); @@ -162,8 +161,12 @@ class SyncManager extends ChangeNotifier with WidgetsBindingObserver { await processQueue(); } + bool _disposed = false; + @override void dispose() { + if (_disposed) return; + _disposed = true; WidgetsBinding.instance.removeObserver(this); super.dispose(); } diff --git a/test/cloud_ride_identity_migration_test.dart b/test/cloud_ride_identity_migration_test.dart index 43fdbbe..0691269 100644 --- a/test/cloud_ride_identity_migration_test.dart +++ b/test/cloud_ride_identity_migration_test.dart @@ -110,6 +110,8 @@ void main() { ); expect(await original.getCloudRideId(originalId), cloudId); + // Insert a dummy ride first so the restored DB assigns a different local ID + await restored.startRide(); final restoredId = await restored.startRide(); await restored.customStatement( 'UPDATE rides SET cloud_ride_id = ? WHERE id = ?', diff --git a/test/maintenance_v102_test.dart b/test/maintenance_v102_test.dart index 2d60ec1..72410df 100644 --- a/test/maintenance_v102_test.dart +++ b/test/maintenance_v102_test.dart @@ -183,12 +183,10 @@ void main() { final syncService = GitHubSyncService(db, auth, prefs, mockClient); - // Perform upload - final result = await syncService.uploadRide(rideId); + // Perform upload via the explicit GitHub PAT export path + final result = await syncService.uploadRideToGitHub(rideId); expect(result.success, isTrue); expect(result.commitSha, equals('c0ffee1234567890')); - expect(syncService.isRideUploaded(rideId), isTrue); - expect(syncService.getRideCommitSha(rideId), equals('c0ffee1234567890')); }); test('6. GitHub Sync Service Graceful Failure & Local Data Preservation', () async { @@ -207,7 +205,7 @@ void main() { final syncService = GitHubSyncService(db, auth, prefs, mockClient); - final result = await syncService.uploadRide(rideId); + final result = await syncService.uploadRideToGitHub(rideId); expect(result.success, isFalse); expect(syncService.state, equals(GitHubUploadState.failed)); expect(result.message.toLowerCase(), contains('failed')); diff --git a/test/sync_architecture_hardening_test.dart b/test/sync_architecture_hardening_test.dart index 5335be6..83a6c4c 100644 --- a/test/sync_architecture_hardening_test.dart +++ b/test/sync_architecture_hardening_test.dart @@ -15,7 +15,7 @@ void main() { group('Sync outbox hardening', () { test('interrupted uploading item is requeued after restart', () async { SharedPreferences.setMockInitialValues({ - 'motorsignal_sync_outbox_manifest_v1': jsonEncode([ + 'motorsignal_sync_outbox_manifest_v2_local': jsonEncode([ SyncItem( localRideId: 42, providerId: 'firebase', @@ -43,7 +43,7 @@ void main() { test('malformed persisted identity is ignored without killing the queue', () async { SharedPreferences.setMockInitialValues({ - 'motorsignal_sync_outbox_manifest_v1': jsonEncode([ + 'motorsignal_sync_outbox_manifest_v2_local': jsonEncode([ {'localRideId': -1, 'providerId': '', 'status': 'queued', 'createdAt': 'bad'}, SyncItem(localRideId: 8, providerId: 'firebase', createdAt: DateTime(2026, 9, 1)).toJson(), ]),