chore(wip): consolidation intermédiaire multi-tickets (sprints Statistiques, UI, Bug resolution, Serveur-client)

Regroupe l'état de travail en cours réalisé dans un même worktree sur
plusieurs tickets/sprints (#85, #136, #145, #155-160, #162-164),
mélangeant des tickets QA et inProgress. Ne constitue pas une feature
terminée : commit de sauvegarde avant triage/split par ticket en
branches feature/* dédiées. Exclut les dossiers d'environnement de
build locaux et le heap dump parasite (.gitignore mis à jour).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 16:48:54 +02:00
parent 58272e354a
commit 917777e18b
279 changed files with 13546 additions and 674 deletions

View File

@ -29,7 +29,7 @@ enum ActiveSetTimerStatus { running, paused, stopped, skipped }
enum ActiveScoreStopwatchStatus { running, paused, stopped }
enum ShareResourceType { program, workoutTemplate }
enum ShareResourceType { program, workoutTemplate, pack }
enum ShareInboxStatus { pending, accepted, declined, revoked }
@ -1476,16 +1476,25 @@ final class WorkoutHistory {
required this.totalActiveMs,
required this.completed,
required this.historySnapshotJson,
this.minHeartRateBpm,
this.averageHeartRateBpm,
this.maxHeartRateBpm,
this.totalDistanceMeters,
this.totalCaloriesKcal,
this.results = const [],
this.stepResults = const [],
}) {
_requireNullablePositive(minHeartRateBpm, 'Min heart rate bpm');
_requireNullablePositiveDouble(
averageHeartRateBpm,
'Average heart rate bpm',
);
_requireNullablePositive(maxHeartRateBpm, 'Max heart rate bpm');
_requireNullableNonNegativeDouble(
totalDistanceMeters,
'Total distance meters',
);
_requireNullableNonNegativeDouble(totalCaloriesKcal, 'Total calories kcal');
}
final EntityMetadata metadata;
@ -1497,15 +1506,21 @@ final class WorkoutHistory {
final int totalActiveMs;
final bool completed;
final String historySnapshotJson;
final int? minHeartRateBpm;
final double? averageHeartRateBpm;
final int? maxHeartRateBpm;
final double? totalDistanceMeters;
final double? totalCaloriesKcal;
final List<WorkoutHistorySetResult> results;
final List<WorkoutHistoryStepResult> stepResults;
WorkoutHistory copyWith({
EntityMetadata? metadata,
Object? minHeartRateBpm = _unchanged,
Object? averageHeartRateBpm = _unchanged,
Object? maxHeartRateBpm = _unchanged,
Object? totalDistanceMeters = _unchanged,
Object? totalCaloriesKcal = _unchanged,
}) {
return WorkoutHistory(
metadata: metadata ?? this.metadata,
@ -1517,18 +1532,149 @@ final class WorkoutHistory {
totalActiveMs: totalActiveMs,
completed: completed,
historySnapshotJson: historySnapshotJson,
minHeartRateBpm: minHeartRateBpm == _unchanged
? this.minHeartRateBpm
: minHeartRateBpm as int?,
averageHeartRateBpm: averageHeartRateBpm == _unchanged
? this.averageHeartRateBpm
: averageHeartRateBpm as double?,
maxHeartRateBpm: maxHeartRateBpm == _unchanged
? this.maxHeartRateBpm
: maxHeartRateBpm as int?,
totalDistanceMeters: totalDistanceMeters == _unchanged
? this.totalDistanceMeters
: totalDistanceMeters as double?,
totalCaloriesKcal: totalCaloriesKcal == _unchanged
? this.totalCaloriesKcal
: totalCaloriesKcal as double?,
results: results,
stepResults: stepResults,
);
}
}
enum WorkoutTelemetryAggregateScope {
session('session'),
exercise('exercise'),
set('set'),
step('step');
const WorkoutTelemetryAggregateScope(this.wireName);
final String wireName;
static WorkoutTelemetryAggregateScope parse(String value) {
for (final scope in values) {
if (scope.wireName == value) {
return scope;
}
}
throw DomainException('Unsupported telemetry scope: $value.');
}
}
final class WorkoutTelemetrySample {
WorkoutTelemetrySample({
required String id,
required String sessionId,
required DateTime capturedAt,
this.programIndex,
this.exerciseIndex,
this.setIndex,
this.passageIndex,
this.stepIndex,
this.programSnapshotId,
this.exerciseSnapshotId,
this.stepSnapshotId,
this.heartRateBpm,
this.distanceMeters,
this.caloriesKcal,
}) : id = _nonBlank(id, 'Telemetry sample id'),
sessionId = _nonBlank(sessionId, 'Telemetry session id'),
capturedAt = capturedAt.toUtc() {
_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 sample must contain at least one metric.',
);
}
}
final String id;
final String sessionId;
final DateTime capturedAt;
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;
}
final class WorkoutTelemetryAggregate {
WorkoutTelemetryAggregate({
required String sessionId,
required this.scope,
this.programIndex,
this.exerciseIndex,
this.setIndex,
this.passageIndex,
this.stepIndex,
required this.sampleCount,
this.minHeartRateBpm,
this.averageHeartRateBpm,
this.maxHeartRateBpm,
this.totalDistanceMeters,
this.totalCaloriesKcal,
}) : sessionId = _nonBlank(sessionId, 'Telemetry aggregate session id') {
_requireNullableNonNegative(programIndex, 'Program index');
_requireNullableNonNegative(exerciseIndex, 'Exercise index');
_requireNullableNonNegative(setIndex, 'Set index');
_requireNullableNonNegative(passageIndex, 'Passage index');
_requireNullableNonNegative(stepIndex, 'Step index');
_requireNonNegative(sampleCount, 'Telemetry sample count');
_requireNullablePositive(minHeartRateBpm, 'Min heart rate bpm');
_requireNullablePositiveDouble(
averageHeartRateBpm,
'Average heart rate bpm',
);
_requireNullablePositive(maxHeartRateBpm, 'Max heart rate bpm');
_requireNullableNonNegativeDouble(
totalDistanceMeters,
'Total distance meters',
);
_requireNullableNonNegativeDouble(totalCaloriesKcal, 'Total calories kcal');
}
final String sessionId;
final WorkoutTelemetryAggregateScope scope;
final int? programIndex;
final int? exerciseIndex;
final int? setIndex;
final int? passageIndex;
final int? stepIndex;
final int sampleCount;
final int? minHeartRateBpm;
final double? averageHeartRateBpm;
final int? maxHeartRateBpm;
final double? totalDistanceMeters;
final double? totalCaloriesKcal;
}
final class WorkoutHistorySetResult {
WorkoutHistorySetResult({
required this.metadata,