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 { Future<void> endRide(int rideId, RidesCompanion data) async {
await (update(rides)..where((t) => t.id.equals(rideId))).write(data.copyWith(endTime: Value(DateTime.now()))); 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() { LazyDatabase _openConnection() {

View File

@ -190,13 +190,13 @@ class _FieldTestReportScreenState extends ConsumerState<FieldTestReportScreen> {
final telemetryIntegrity = s != null ? s.validPercentage.toStringAsFixed(1) : '100.0'; final telemetryIntegrity = s != null ? s.validPercentage.toStringAsFixed(1) : '100.0';
final gpsIntegrity = s != null && s.totalSamples > 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'; : '100.0';
final sensorIntegrity = s != null && s.totalSamples > 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'; : '100.0';
final dbIntegrity = s != null && s.totalSamples > 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'; : '100.0';
return Card( return Card(

View File

@ -804,7 +804,7 @@ class FieldTestLogger {
databaseSamplesLost: drift.Value(_droppedEventsCount), databaseSamplesLost: drift.Value(_droppedEventsCount),
eventCount: drift.Value(events.length), eventCount: drift.Value(events.length),
overallConfidence: drift.Value(qualityReport.overallScore / 100.0), overallConfidence: drift.Value(qualityReport.overallScore / 100.0),
rideQualityScore: drift.Value(qualityReport.overallScore), rideQualityScore: drift.Value(qualityReport.overallScore.toDouble()),
rideQualityRating: drift.Value(qualityReport.rating), rideQualityRating: drift.Value(qualityReport.rating),
wasRecoveredAfterCrash: drift.Value(wasRecovered), wasRecoveredAfterCrash: drift.Value(wasRecovered),
wasInterrupted: drift.Value(ride?.endTime == null || 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 '../../core/version/app_identity.dart';
import '../../database/database.dart'; import '../../database/database.dart';
import '../../features/records/records_service.dart'; import '../../features/records/records_service.dart';
import '../auth/auth_providers.dart';
import '../auth/auth_service.dart'; import '../auth/auth_service.dart';
import '../export_service.dart'; import '../export_service.dart';
import '../field_test/field_test_export_service.dart'; import '../field_test/field_test_export_service.dart';
@ -75,12 +76,15 @@ class GitHubSyncService extends ChangeNotifier {
bool isRideUploaded(int rideId) { bool isRideUploaded(int rideId) {
final key = _userScopedKey('motorcloud_uploaded_ride', 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) { String? getRideCommitSha(int rideId) {
final key = _userScopedKey('motorcloud_ride_remote_id', 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) { String? getRideUploadTimestamp(int rideId) {

View File

@ -1,4 +1,5 @@
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:drift/drift.dart' show Variable;
import '../../database/database.dart'; import '../../database/database.dart';
import '../auth/auth_service.dart'; import '../auth/auth_service.dart';
@ -34,12 +35,10 @@ class CloudReconciliationService {
final FirebaseFirestore _firestore; final FirebaseFirestore _firestore;
CloudReconciliationService({ CloudReconciliationService({
required AppDatabase db, required AppDatabase this._db,
required AuthService authService, required AuthService this._authService,
FirebaseFirestore? firestore, FirebaseFirestore? firestore,
}) : _db = db, }) : _firestore = firestore ?? FirebaseFirestore.instance;
_authService = authService,
_firestore = firestore ?? FirebaseFirestore.instance;
Future<CloudReconciliationResult> reconcileMetadata() async { Future<CloudReconciliationResult> reconcileMetadata() async {
final user = _authService.currentUser; final user = _authService.currentUser;
@ -194,8 +193,8 @@ class CloudReconciliationService {
// Only flag meaningful numeric divergence. Missing cloud fields are not // Only flag meaningful numeric divergence. Missing cloud fields are not
// treated as authoritative and therefore cannot overwrite local values. // treated as authoritative and therefore cannot overwrite local values.
if (cloudDistance != null && (localDistance - cloudDistance).abs() > 0.0001) return true; if (cloudDistance != null && localDistance != null && (localDistance - cloudDistance).abs() > 0.0001) return true;
if (cloudTopSpeed != null && (localTopSpeed - cloudTopSpeed).abs() > 0.0001) return true; if (cloudTopSpeed != null && localTopSpeed != null && (localTopSpeed - cloudTopSpeed).abs() > 0.0001) return true;
return false; return false;
} }

View File

@ -16,9 +16,8 @@ class FirebaseSyncProvider implements SyncProvider {
final R2UploadService _r2UploadService; final R2UploadService _r2UploadService;
final SyncBundleWorker _bundleWorker; 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, : _db = db,
_authService = authService,
_firestore = firestore ?? _resolveFirestore(), _firestore = firestore ?? _resolveFirestore(),
_r2UploadService = r2UploadService ?? R2UploadService(), _r2UploadService = r2UploadService ?? R2UploadService(),
_bundleWorker = bundleWorker ?? SyncBundleWorker(db); _bundleWorker = bundleWorker ?? SyncBundleWorker(db);

View File

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

View File

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'sync_models.dart'; import 'sync_models.dart';
import 'sync_outbox.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()) { SyncManager(this._outbox, {List<SyncProvider>? initialProviders, String? defaultProviderId, DateTime? Function()? nowProvider}) : _nowProvider = nowProvider ?? (() => DateTime.now()) {
if (initialProviders != null) { if (initialProviders != null) {
for (final p in initialProviders) registerProvider(p); for (final p in initialProviders) { registerProvider(p); }
} }
if (defaultProviderId != null) _activeProviderId = defaultProviderId; if (defaultProviderId != null) _activeProviderId = defaultProviderId;
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
@ -162,8 +161,12 @@ class SyncManager extends ChangeNotifier with WidgetsBindingObserver {
await processQueue(); await processQueue();
} }
bool _disposed = false;
@override @override
void dispose() { void dispose() {
if (_disposed) return;
_disposed = true;
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
super.dispose(); super.dispose();
} }

View File

@ -110,6 +110,8 @@ void main() {
); );
expect(await original.getCloudRideId(originalId), cloudId); 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(); final restoredId = await restored.startRide();
await restored.customStatement( await restored.customStatement(
'UPDATE rides SET cloud_ride_id = ? WHERE id = ?', 'UPDATE rides SET cloud_ride_id = ? WHERE id = ?',

View File

@ -183,12 +183,10 @@ void main() {
final syncService = GitHubSyncService(db, auth, prefs, mockClient); final syncService = GitHubSyncService(db, auth, prefs, mockClient);
// Perform upload // Perform upload via the explicit GitHub PAT export path
final result = await syncService.uploadRide(rideId); final result = await syncService.uploadRideToGitHub(rideId);
expect(result.success, isTrue); expect(result.success, isTrue);
expect(result.commitSha, equals('c0ffee1234567890')); 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 { 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 syncService = GitHubSyncService(db, auth, prefs, mockClient);
final result = await syncService.uploadRide(rideId); final result = await syncService.uploadRideToGitHub(rideId);
expect(result.success, isFalse); expect(result.success, isFalse);
expect(syncService.state, equals(GitHubUploadState.failed)); expect(syncService.state, equals(GitHubUploadState.failed));
expect(result.message.toLowerCase(), contains('failed')); expect(result.message.toLowerCase(), contains('failed'));

View File

@ -15,7 +15,7 @@ void main() {
group('Sync outbox hardening', () { group('Sync outbox hardening', () {
test('interrupted uploading item is requeued after restart', () async { test('interrupted uploading item is requeued after restart', () async {
SharedPreferences.setMockInitialValues({ SharedPreferences.setMockInitialValues({
'motorsignal_sync_outbox_manifest_v1': jsonEncode([ 'motorsignal_sync_outbox_manifest_v2_local': jsonEncode([
SyncItem( SyncItem(
localRideId: 42, localRideId: 42,
providerId: 'firebase', providerId: 'firebase',
@ -43,7 +43,7 @@ void main() {
test('malformed persisted identity is ignored without killing the queue', () async { test('malformed persisted identity is ignored without killing the queue', () async {
SharedPreferences.setMockInitialValues({ SharedPreferences.setMockInitialValues({
'motorsignal_sync_outbox_manifest_v1': jsonEncode([ 'motorsignal_sync_outbox_manifest_v2_local': jsonEncode([
{'localRideId': -1, 'providerId': '', 'status': 'queued', 'createdAt': 'bad'}, {'localRideId': -1, 'providerId': '', 'status': 'queued', 'createdAt': 'bad'},
SyncItem(localRideId: 8, providerId: 'firebase', createdAt: DateTime(2026, 9, 1)).toJson(), SyncItem(localRideId: 8, providerId: 'firebase', createdAt: DateTime(2026, 9, 1)).toJson(),
]), ]),