chore: establish clean-build repair baseline

This commit is contained in:
Developer 2026-09-02 19:27:23 -04:00
parent e5f2f837fb
commit 84073e6a73
12 changed files with 44 additions and 25 deletions

View File

@ -0,0 +1,10 @@
name: New MCP server
version: 0.0.1
schema: v1
mcpServers:
- name: New MCP server
command: npx
args:
- -y
- <your-mcp-server>
env: {}

View File

@ -151,6 +151,11 @@ class AppDatabase extends _$AppDatabase {
Future<void> 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<int> addPerformanceRun(PerformanceRunsCompanion entry) {
return into(performanceRuns).insert(entry);
}
}
LazyDatabase _openConnection() {

View File

@ -190,13 +190,13 @@ class _FieldTestReportScreenState extends ConsumerState<FieldTestReportScreen> {
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(

View File

@ -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),

View File

@ -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) {

View File

@ -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<CloudReconciliationResult> 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;
}

View File

@ -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);

View File

@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'package:flutter/foundation.dart';

View File

@ -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<SyncProvider>? 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();
}

View File

@ -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 = ?',

View File

@ -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'));

View File

@ -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(),
]),