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

@ -950,25 +950,38 @@ final class DriftWorkoutTemplateRepository
@override
Future<void> save(domain.WorkoutTemplate template) async {
await database.transaction(() async {
await _upsertWithChangeLog(
database: database,
tableName: 'workout_templates',
entityType: 'WorkoutTemplate',
metadata: template.metadata,
write: () => database
.into(database.workoutTemplates)
.insertOnConflictUpdate(_workoutTemplateCompanion(template)),
);
await _writeWorkoutTemplateStarterMetadata(database, template);
for (final program in template.programs) {
await saveProgram(program);
}
for (final override in template.overrides) {
await saveOverride(override);
await _saveTemplate(template);
});
}
@override
Future<void> saveAll(List<domain.WorkoutTemplate> templates) async {
await database.transaction(() async {
for (final template in templates) {
await _saveTemplate(template);
}
});
}
Future<void> _saveTemplate(domain.WorkoutTemplate template) async {
await _upsertWithChangeLog(
database: database,
tableName: 'workout_templates',
entityType: 'WorkoutTemplate',
metadata: template.metadata,
write: () => database
.into(database.workoutTemplates)
.insertOnConflictUpdate(_workoutTemplateCompanion(template)),
);
await _writeWorkoutTemplateStarterMetadata(database, template);
for (final program in template.programs) {
await saveProgram(program);
}
for (final override in template.overrides) {
await saveOverride(override);
}
}
@override
Future<void> saveProgram(domain.WorkoutTemplateProgram program) async {
await _upsertWithChangeLog(
@ -1614,11 +1627,18 @@ final class DriftWorkoutHistoryRepository implements WorkoutHistoryRepository {
@override
Future<void> patchHeartRateSummary({
required String historyId,
int? minHeartRateBpm,
required double averageHeartRateBpm,
required int maxHeartRateBpm,
double? totalDistanceMeters,
double? totalCaloriesKcal,
required DateTime patchedAt,
}) async {
if (averageHeartRateBpm <= 0 || maxHeartRateBpm <= 0) {
if ((minHeartRateBpm != null && minHeartRateBpm <= 0) ||
averageHeartRateBpm <= 0 ||
maxHeartRateBpm <= 0 ||
(totalDistanceMeters != null && totalDistanceMeters < 0) ||
(totalCaloriesKcal != null && totalCaloriesKcal < 0)) {
return;
}
final row =
@ -1626,8 +1646,11 @@ final class DriftWorkoutHistoryRepository implements WorkoutHistoryRepository {
(table) =>
table.id.equals(historyId) &
table.deletedAt.isNull() &
table.minHeartRateBpm.isNull() &
table.averageHeartRateBpm.isNull() &
table.maxHeartRateBpm.isNull(),
table.maxHeartRateBpm.isNull() &
table.totalDistanceMeters.isNull() &
table.totalCaloriesKcal.isNull(),
))
.getSingleOrNull();
if (row == null) {
@ -1641,8 +1664,11 @@ final class DriftWorkoutHistoryRepository implements WorkoutHistoryRepository {
updatedAt: Value(patchedAt.toUtc()),
syncState: const Value('dirty'),
localRevision: Value(revision),
minHeartRateBpm: Value(minHeartRateBpm),
averageHeartRateBpm: Value(averageHeartRateBpm),
maxHeartRateBpm: Value(maxHeartRateBpm),
totalDistanceMeters: Value(totalDistanceMeters),
totalCaloriesKcal: Value(totalCaloriesKcal),
),
);
await _writeChangeLog(
@ -1736,6 +1762,103 @@ final class DriftWorkoutHistoryRepository implements WorkoutHistoryRepository {
}
}
final class DriftWorkoutTelemetryRepository
implements WorkoutTelemetryRepository {
const DriftWorkoutTelemetryRepository(this.database);
final db.AppDatabase database;
@override
Future<bool> saveSample(domain.WorkoutTelemetrySample sample) async {
final existing = await (database.select(
database.workoutTelemetrySamples,
)..where((table) => table.id.equals(sample.id))).getSingleOrNull();
if (existing != null) {
return false;
}
final inserted = await database
.into(database.workoutTelemetrySamples)
.insert(
_workoutTelemetrySampleCompanion(sample),
mode: InsertMode.insertOrIgnore,
);
return inserted > 0;
}
@override
Future<List<domain.WorkoutTelemetrySample>> listSamples(
String sessionId,
) async {
final rows =
await (database.select(database.workoutTelemetrySamples)
..where((table) => table.sessionId.equals(sessionId))
..orderBy([(table) => OrderingTerm.asc(table.capturedAt)]))
.get();
return rows.map(_workoutTelemetrySampleFromRow).toList();
}
@override
Future<void> replaceAggregatesForSession({
required String sessionId,
required List<domain.WorkoutTelemetryAggregate> aggregates,
}) async {
await database.transaction(() async {
await (database.delete(
database.workoutTelemetryAggregates,
)..where((table) => table.sessionId.equals(sessionId))).go();
if (aggregates.isEmpty) {
return;
}
await database.batch((batch) {
batch.insertAll(
database.workoutTelemetryAggregates,
aggregates.map(_workoutTelemetryAggregateCompanion).toList(),
);
});
});
}
@override
Future<List<domain.WorkoutTelemetryAggregate>> listAggregates(
String sessionId,
) async {
final rows = await (database.select(
database.workoutTelemetryAggregates,
)..where((table) => table.sessionId.equals(sessionId))).get();
return rows.map(_workoutTelemetryAggregateFromRow).toList();
}
@override
Future<domain.WorkoutTelemetryAggregate?> findAggregate({
required String sessionId,
required domain.WorkoutTelemetryAggregateScope scope,
int? programIndex,
int? exerciseIndex,
int? setIndex,
int? passageIndex,
int? stepIndex,
}) async {
final rows =
await (database.select(database.workoutTelemetryAggregates)..where(
(table) =>
table.sessionId.equals(sessionId) &
table.scope.equals(scope.wireName),
))
.get();
for (final row in rows) {
final aggregate = _workoutTelemetryAggregateFromRow(row);
if (aggregate.programIndex == programIndex &&
aggregate.exerciseIndex == exerciseIndex &&
aggregate.setIndex == setIndex &&
aggregate.passageIndex == passageIndex &&
aggregate.stepIndex == stepIndex) {
return aggregate;
}
}
return null;
}
}
final class DriftLocalDataBackupRepository
implements LocalDataBackupRepository {
const DriftLocalDataBackupRepository(this.database);
@ -4363,8 +4486,52 @@ db.WorkoutHistoriesCompanion _workoutHistoryCompanion(
totalActiveMs: Value(history.totalActiveMs),
completed: Value(history.completed),
historySnapshotJson: Value(history.historySnapshotJson),
minHeartRateBpm: Value(history.minHeartRateBpm),
averageHeartRateBpm: Value(history.averageHeartRateBpm),
maxHeartRateBpm: Value(history.maxHeartRateBpm),
totalDistanceMeters: Value(history.totalDistanceMeters),
totalCaloriesKcal: Value(history.totalCaloriesKcal),
);
}
db.WorkoutTelemetrySamplesCompanion _workoutTelemetrySampleCompanion(
domain.WorkoutTelemetrySample sample,
) {
return db.WorkoutTelemetrySamplesCompanion.insert(
id: sample.id,
sessionId: sample.sessionId,
capturedAt: sample.capturedAt.toUtc(),
programIndex: Value(sample.programIndex),
exerciseIndex: Value(sample.exerciseIndex),
setIndex: Value(sample.setIndex),
passageIndex: Value(sample.passageIndex),
stepIndex: Value(sample.stepIndex),
programSnapshotId: Value(sample.programSnapshotId),
exerciseSnapshotId: Value(sample.exerciseSnapshotId),
stepSnapshotId: Value(sample.stepSnapshotId),
heartRateBpm: Value(sample.heartRateBpm),
distanceMeters: Value(sample.distanceMeters),
caloriesKcal: Value(sample.caloriesKcal),
);
}
db.WorkoutTelemetryAggregatesCompanion _workoutTelemetryAggregateCompanion(
domain.WorkoutTelemetryAggregate aggregate,
) {
return db.WorkoutTelemetryAggregatesCompanion.insert(
sessionId: aggregate.sessionId,
scope: aggregate.scope.wireName,
programIndex: Value(aggregate.programIndex),
exerciseIndex: Value(aggregate.exerciseIndex),
setIndex: Value(aggregate.setIndex),
passageIndex: Value(aggregate.passageIndex),
stepIndex: Value(aggregate.stepIndex),
sampleCount: aggregate.sampleCount,
minHeartRateBpm: Value(aggregate.minHeartRateBpm),
averageHeartRateBpm: Value(aggregate.averageHeartRateBpm),
maxHeartRateBpm: Value(aggregate.maxHeartRateBpm),
totalDistanceMeters: Value(aggregate.totalDistanceMeters),
totalCaloriesKcal: Value(aggregate.totalCaloriesKcal),
);
}
@ -4480,13 +4647,57 @@ domain.WorkoutHistory _workoutHistoryFromRow(
totalActiveMs: row.totalActiveMs,
completed: row.completed,
historySnapshotJson: row.historySnapshotJson,
minHeartRateBpm: row.minHeartRateBpm,
averageHeartRateBpm: row.averageHeartRateBpm,
maxHeartRateBpm: row.maxHeartRateBpm,
totalDistanceMeters: row.totalDistanceMeters,
totalCaloriesKcal: row.totalCaloriesKcal,
results: results,
stepResults: stepResults,
);
}
domain.WorkoutTelemetrySample _workoutTelemetrySampleFromRow(
db.WorkoutTelemetrySample row,
) {
return domain.WorkoutTelemetrySample(
id: row.id,
sessionId: row.sessionId,
capturedAt: _utc(row.capturedAt),
programIndex: row.programIndex,
exerciseIndex: row.exerciseIndex,
setIndex: row.setIndex,
passageIndex: row.passageIndex,
stepIndex: row.stepIndex,
programSnapshotId: row.programSnapshotId,
exerciseSnapshotId: row.exerciseSnapshotId,
stepSnapshotId: row.stepSnapshotId,
heartRateBpm: row.heartRateBpm,
distanceMeters: row.distanceMeters,
caloriesKcal: row.caloriesKcal,
);
}
domain.WorkoutTelemetryAggregate _workoutTelemetryAggregateFromRow(
db.WorkoutTelemetryAggregate row,
) {
return domain.WorkoutTelemetryAggregate(
sessionId: row.sessionId,
scope: domain.WorkoutTelemetryAggregateScope.parse(row.scope),
programIndex: row.programIndex,
exerciseIndex: row.exerciseIndex,
setIndex: row.setIndex,
passageIndex: row.passageIndex,
stepIndex: row.stepIndex,
sampleCount: row.sampleCount,
minHeartRateBpm: row.minHeartRateBpm,
averageHeartRateBpm: row.averageHeartRateBpm,
maxHeartRateBpm: row.maxHeartRateBpm,
totalDistanceMeters: row.totalDistanceMeters,
totalCaloriesKcal: row.totalCaloriesKcal,
);
}
domain.WorkoutHistorySetResult _workoutHistorySetResultFromRow(
db.WorkoutHistorySetResult row,
) {
@ -4739,8 +4950,11 @@ Map<String, Object?> _workoutHistoryPayload(domain.WorkoutHistory history) => {
'totalActiveMs': history.totalActiveMs,
'completed': history.completed,
'historySnapshotJson': history.historySnapshotJson,
'minHeartRateBpm': history.minHeartRateBpm,
'averageHeartRateBpm': history.averageHeartRateBpm,
'maxHeartRateBpm': history.maxHeartRateBpm,
'totalDistanceMeters': history.totalDistanceMeters,
'totalCaloriesKcal': history.totalCaloriesKcal,
};
Map<String, Object?> _localWorkoutHistoryPayload(
@ -4895,8 +5109,11 @@ domain.WorkoutHistory _workoutHistoryFromLocalBackupPayload(
completed: payload['completed'] as bool? ?? false,
historySnapshotJson:
payload['historySnapshotJson'] as String? ?? '{"programs":[]}',
minHeartRateBpm: payload['minHeartRateBpm'] as int?,
averageHeartRateBpm: (payload['averageHeartRateBpm'] as num?)?.toDouble(),
maxHeartRateBpm: payload['maxHeartRateBpm'] as int?,
totalDistanceMeters: (payload['totalDistanceMeters'] as num?)?.toDouble(),
totalCaloriesKcal: (payload['totalCaloriesKcal'] as num?)?.toDouble(),
results: _workoutHistorySetResultsFromPayload(payload['results'], metadata),
stepResults: _workoutHistoryStepResultsFromPayload(
payload['stepResults'],
@ -5412,12 +5629,14 @@ OnlineSyncStatus _onlineSyncStatusFromDb(String value) => switch (value) {
String _shareResourceTypeToDb(domain.ShareResourceType type) => switch (type) {
domain.ShareResourceType.program => 'program',
domain.ShareResourceType.workoutTemplate => 'workoutTemplate',
domain.ShareResourceType.pack => 'pack',
};
domain.ShareResourceType _shareResourceTypeFromDb(String value) =>
switch (value) {
'program' => domain.ShareResourceType.program,
'workoutTemplate' => domain.ShareResourceType.workoutTemplate,
'pack' => domain.ShareResourceType.pack,
_ => throw domain.DomainException('Unknown share resource type: $value'),
};