Ajoute la synchronisation client incrémentale LWW vers l'API serveur (application/use_cases.dart: SyncUseCases, ports.dart, migration Drift schemaVersion 10→11 pour les métadonnées de sync et mappings de ressources, infrastructure/remote/sync_api.dart) et l'écran de profil avec entrée sur l'accueil (presentation/profile_screen.dart, home_screen.dart, presentation.dart). Développés dans le même worktree partagé par DevBackend et DevFrontend ; commit combiné car test/presentation/home_screen_test.dart mélange authentiquement les deux tickets (le test de l'entrée Profil et la mise à jour du fake de bootstrap requise par le nouveau getter syncUseCases sur AppDependencies). Corrige au passage une couleur `crimson` inexistante dans le thème (remplacée par colorScheme.error) et une signature de paramètres positionnels/nommés incohérente sur un fake de test. flutter pub get OK, build_runner OK, dart format appliqué, analyze propre (mêmes infos préexistantes), 119/119 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -73,6 +73,194 @@ abstract interface class RemoteAuthApi {
|
||||
Future<void> logout(String token);
|
||||
}
|
||||
|
||||
enum SyncResourceType {
|
||||
exercise,
|
||||
program,
|
||||
workoutTemplate,
|
||||
workoutHistory,
|
||||
mediaAsset,
|
||||
}
|
||||
|
||||
enum RemoteSyncPushStatus { accepted, ignoredOlder, error }
|
||||
|
||||
enum OnlineSyncStatus { idle, syncing, success, failure }
|
||||
|
||||
final class RemoteSyncPushItem {
|
||||
const RemoteSyncPushItem({
|
||||
required this.resourceType,
|
||||
required this.clientId,
|
||||
required this.schemaVersion,
|
||||
required this.clientUpdatedAt,
|
||||
required this.deletedAt,
|
||||
required this.payload,
|
||||
});
|
||||
|
||||
final SyncResourceType resourceType;
|
||||
final String clientId;
|
||||
final int schemaVersion;
|
||||
final DateTime clientUpdatedAt;
|
||||
final DateTime? deletedAt;
|
||||
final Map<String, Object?> payload;
|
||||
}
|
||||
|
||||
final class RemoteSyncPushItemResult {
|
||||
const RemoteSyncPushItemResult({
|
||||
required this.resourceType,
|
||||
required this.clientId,
|
||||
this.serverId,
|
||||
required this.status,
|
||||
this.serverUpdatedAt,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
final SyncResourceType resourceType;
|
||||
final String clientId;
|
||||
final String? serverId;
|
||||
final RemoteSyncPushStatus status;
|
||||
final DateTime? serverUpdatedAt;
|
||||
final String? errorMessage;
|
||||
}
|
||||
|
||||
final class RemoteSyncPushResult {
|
||||
const RemoteSyncPushResult({
|
||||
required this.serverCursor,
|
||||
required this.results,
|
||||
});
|
||||
|
||||
final String? serverCursor;
|
||||
final List<RemoteSyncPushItemResult> results;
|
||||
}
|
||||
|
||||
final class RemoteSyncedItem {
|
||||
const RemoteSyncedItem({
|
||||
required this.resourceType,
|
||||
required this.clientId,
|
||||
required this.serverId,
|
||||
required this.schemaVersion,
|
||||
required this.clientUpdatedAt,
|
||||
required this.serverUpdatedAt,
|
||||
required this.deletedAt,
|
||||
required this.payload,
|
||||
});
|
||||
|
||||
final SyncResourceType resourceType;
|
||||
final String clientId;
|
||||
final String serverId;
|
||||
final int schemaVersion;
|
||||
final DateTime clientUpdatedAt;
|
||||
final DateTime serverUpdatedAt;
|
||||
final DateTime? deletedAt;
|
||||
final Map<String, Object?> payload;
|
||||
}
|
||||
|
||||
final class RemoteSyncPullResult {
|
||||
const RemoteSyncPullResult({required this.serverCursor, required this.items});
|
||||
|
||||
final String? serverCursor;
|
||||
final List<RemoteSyncedItem> items;
|
||||
}
|
||||
|
||||
abstract interface class RemoteSyncApi {
|
||||
Future<RemoteSyncPushResult> push({
|
||||
required String deviceId,
|
||||
required List<RemoteSyncPushItem> items,
|
||||
required String token,
|
||||
});
|
||||
|
||||
Future<RemoteSyncPullResult> pull({
|
||||
required String? since,
|
||||
required String token,
|
||||
});
|
||||
}
|
||||
|
||||
final class SyncMetadataSnapshot {
|
||||
const SyncMetadataSnapshot({
|
||||
this.serverCursor,
|
||||
this.lastSuccessfulSyncAt,
|
||||
this.lastAttemptAt,
|
||||
this.lastFailureAt,
|
||||
this.status = OnlineSyncStatus.idle,
|
||||
this.pendingPushCount,
|
||||
});
|
||||
|
||||
final String? serverCursor;
|
||||
final DateTime? lastSuccessfulSyncAt;
|
||||
final DateTime? lastAttemptAt;
|
||||
final DateTime? lastFailureAt;
|
||||
final OnlineSyncStatus status;
|
||||
final int? pendingPushCount;
|
||||
|
||||
SyncMetadataSnapshot copyWith({
|
||||
Object? serverCursor = _portsUnchanged,
|
||||
Object? lastSuccessfulSyncAt = _portsUnchanged,
|
||||
Object? lastAttemptAt = _portsUnchanged,
|
||||
Object? lastFailureAt = _portsUnchanged,
|
||||
OnlineSyncStatus? status,
|
||||
Object? pendingPushCount = _portsUnchanged,
|
||||
}) {
|
||||
return SyncMetadataSnapshot(
|
||||
serverCursor: serverCursor == _portsUnchanged
|
||||
? this.serverCursor
|
||||
: serverCursor as String?,
|
||||
lastSuccessfulSyncAt: lastSuccessfulSyncAt == _portsUnchanged
|
||||
? this.lastSuccessfulSyncAt
|
||||
: lastSuccessfulSyncAt as DateTime?,
|
||||
lastAttemptAt: lastAttemptAt == _portsUnchanged
|
||||
? this.lastAttemptAt
|
||||
: lastAttemptAt as DateTime?,
|
||||
lastFailureAt: lastFailureAt == _portsUnchanged
|
||||
? this.lastFailureAt
|
||||
: lastFailureAt as DateTime?,
|
||||
status: status ?? this.status,
|
||||
pendingPushCount: pendingPushCount == _portsUnchanged
|
||||
? this.pendingPushCount
|
||||
: pendingPushCount as int?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final class RemoteResourceMapping {
|
||||
const RemoteResourceMapping({
|
||||
required this.resourceType,
|
||||
required this.clientId,
|
||||
required this.serverId,
|
||||
required this.serverUpdatedAt,
|
||||
});
|
||||
|
||||
final SyncResourceType resourceType;
|
||||
final String clientId;
|
||||
final String serverId;
|
||||
final DateTime serverUpdatedAt;
|
||||
}
|
||||
|
||||
final class PendingSyncChange {
|
||||
const PendingSyncChange({required this.changeLogIds, required this.item});
|
||||
|
||||
final List<String> changeLogIds;
|
||||
final RemoteSyncPushItem item;
|
||||
}
|
||||
|
||||
abstract interface class SyncMetadataRepository {
|
||||
Future<SyncMetadataSnapshot> read();
|
||||
Future<void> save(SyncMetadataSnapshot metadata);
|
||||
}
|
||||
|
||||
abstract interface class RemoteResourceMappingRepository {
|
||||
Future<RemoteResourceMapping?> find({
|
||||
required SyncResourceType resourceType,
|
||||
required String clientId,
|
||||
});
|
||||
Future<void> save(RemoteResourceMapping mapping);
|
||||
}
|
||||
|
||||
abstract interface class LocalSyncChangeRepository {
|
||||
Future<List<PendingSyncChange>> listPendingChanges();
|
||||
Future<void> markChangesSynced(List<String> changeLogIds, DateTime syncedAt);
|
||||
Future<bool> applyRemoteItem(RemoteSyncedItem item);
|
||||
}
|
||||
|
||||
const Object _portsUnchanged = Object();
|
||||
|
||||
abstract interface class ExerciseRepository {
|
||||
Future<Exercise?> findById(String id);
|
||||
Future<List<Exercise>> listActive();
|
||||
@ -189,10 +377,16 @@ final class SyncRunSummary {
|
||||
const SyncRunSummary({
|
||||
required this.pushedChanges,
|
||||
required this.pulledChanges,
|
||||
this.skipped = false,
|
||||
this.failed = false,
|
||||
this.message,
|
||||
});
|
||||
|
||||
final int pushedChanges;
|
||||
final int pulledChanges;
|
||||
final bool skipped;
|
||||
final bool failed;
|
||||
final String? message;
|
||||
}
|
||||
|
||||
abstract interface class SyncGateway {
|
||||
|
||||
Reference in New Issue
Block a user