feat(server): implemente synchronisation et serveur avec fixtures (#187)

- Implémente la couche de synchronisation avec le serveur
- Ajoute les fixtures versionnées pour les tests
- Met à jour Drift database et repositories pour le support sync
- Améliore les tests de synchronisation
- Corrige et améliore le watch companion pour la collecte de métriques

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 23:07:31 +02:00
parent d1edb2397e
commit fe3608cc4a
31 changed files with 5056 additions and 394 deletions

View File

@ -1681,6 +1681,76 @@ final class WorkoutTelemetrySample {
final double? caloriesKcal;
}
final class ActiveWorkoutTelemetryWindowState {
ActiveWorkoutTelemetryWindowState({
required String sessionId,
required this.windowStartedActiveMs,
required this.latestCapturedAt,
this.programIndex,
this.exerciseIndex,
this.setIndex,
this.passageIndex,
this.stepIndex,
this.programSnapshotId,
this.exerciseSnapshotId,
this.stepSnapshotId,
this.heartRateBpm,
this.distanceMeters,
this.caloriesKcal,
}) : sessionId = _nonBlank(sessionId, 'Telemetry window session id') {
_requireNonNegative(windowStartedActiveMs, 'Telemetry window active ms');
_requireNullableNonNegative(programIndex, 'Program index');
_requireNullableNonNegative(exerciseIndex, 'Exercise index');
_requireNullableNonNegative(setIndex, 'Set index');
_requireNullableNonNegative(passageIndex, 'Passage index');
_requireNullableNonNegative(stepIndex, 'Step index');
_requireNullablePositive(heartRateBpm, 'Heart rate bpm');
_requireNullableNonNegativeDouble(distanceMeters, 'Distance meters');
_requireNullableNonNegativeDouble(caloriesKcal, 'Calories kcal');
if (heartRateBpm == null &&
distanceMeters == null &&
caloriesKcal == null) {
throw const DomainException(
'Telemetry window state must contain at least one metric.',
);
}
}
final String sessionId;
final int windowStartedActiveMs;
final DateTime latestCapturedAt;
final int? programIndex;
final int? exerciseIndex;
final int? setIndex;
final int? passageIndex;
final int? stepIndex;
final String? programSnapshotId;
final String? exerciseSnapshotId;
final String? stepSnapshotId;
final int? heartRateBpm;
final double? distanceMeters;
final double? caloriesKcal;
WorkoutTelemetrySample toSample(String id) {
return WorkoutTelemetrySample(
id: id,
sessionId: sessionId,
capturedAt: latestCapturedAt,
programIndex: programIndex,
exerciseIndex: exerciseIndex,
setIndex: setIndex,
passageIndex: passageIndex,
stepIndex: stepIndex,
programSnapshotId: programSnapshotId,
exerciseSnapshotId: exerciseSnapshotId,
stepSnapshotId: stepSnapshotId,
heartRateBpm: heartRateBpm,
distanceMeters: distanceMeters,
caloriesKcal: caloriesKcal,
);
}
}
final class WorkoutTelemetryAggregate {
WorkoutTelemetryAggregate({
required String sessionId,
@ -1793,6 +1863,51 @@ final class WorkoutTelemetryGraphSeries {
final int? maxHeartRateBpm;
}
final class ScopeInstanceDescriptor {
ScopeInstanceDescriptor({
required this.scope,
this.programIndex,
this.exerciseIndex,
this.setIndex,
this.passageIndex,
this.stepIndex,
required this.ordinal,
required DateTime firstCapturedAt,
}) : firstCapturedAt = firstCapturedAt.toUtc() {
_requireNullableNonNegative(programIndex, 'Program index');
_requireNullableNonNegative(exerciseIndex, 'Exercise index');
_requireNullableNonNegative(setIndex, 'Set index');
_requireNullableNonNegative(passageIndex, 'Passage index');
_requireNullableNonNegative(stepIndex, 'Step index');
_requirePositive(ordinal, 'Scope instance ordinal');
}
final WorkoutTelemetryAggregateScope scope;
final int? programIndex;
final int? exerciseIndex;
final int? setIndex;
final int? passageIndex;
final int? stepIndex;
final int ordinal;
final DateTime firstCapturedAt;
}
enum ScopeMarkerBoundary { start, end }
final class ScopeMarker {
ScopeMarker({
required this.elapsedMs,
required String label,
required this.boundary,
}) : label = _nonBlank(label, 'Scope marker label') {
_requireNonNegative(elapsedMs, 'Scope marker elapsed ms');
}
final int elapsedMs;
final String label;
final ScopeMarkerBoundary boundary;
}
final class WorkoutHistorySetResult {
WorkoutHistorySetResult({
required this.metadata,