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

@ -28,6 +28,8 @@ part 'app_database.g.dart';
RemoteResourceMappings,
ShareInboxItems,
SyncMetadataEntries,
WorkoutTelemetryAggregates,
WorkoutTelemetrySamples,
WorkoutHistories,
WorkoutHistorySetResults,
WorkoutHistoryStepResults,
@ -49,7 +51,7 @@ final class AppDatabase extends _$AppDatabase {
}
@override
int get schemaVersion => 22;
int get schemaVersion => 24;
@override
MigrationStrategy get migration {
@ -129,6 +131,12 @@ final class AppDatabase extends _$AppDatabase {
if (from < 22) {
await _migrateToSchema22();
}
if (from < 23) {
await _migrateToSchema23();
}
if (from < 24) {
await _migrateToSchema24(migrator);
}
await _createIndexes();
},
beforeOpen: (details) async {
@ -138,6 +146,9 @@ final class AppDatabase extends _$AppDatabase {
}
Future<void> _createIndexes() async {
if (!await _hasTable(_syncableTableNames.first)) {
return;
}
for (final tableName in _syncableTableNames) {
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_${tableName}_deleted_at '
@ -250,6 +261,14 @@ final class AppDatabase extends _$AppDatabase {
'ON workout_history (completed, started_at) '
'WHERE deleted_at IS NULL',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_workout_telemetry_samples_session '
'ON workout_telemetry_samples (session_id, captured_at)',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_workout_telemetry_aggregates_session '
'ON workout_telemetry_aggregates (session_id, scope)',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_workout_history_set_results_history_id '
'ON workout_history_set_results (workout_history_id)',
@ -816,6 +835,127 @@ CREATE TABLE IF NOT EXISTS active_set_timer_states (
);
}
Future<void> _migrateToSchema23() async {
await customStatement('''
CREATE TABLE IF NOT EXISTS share_inbox_items_v23 (
share_id TEXT NOT NULL PRIMARY KEY,
sender_user_id TEXT NOT NULL,
resource_type TEXT NOT NULL CHECK (
resource_type IN ('program', 'workoutTemplate', 'pack')
),
payload_json TEXT NOT NULL,
status TEXT NOT NULL CHECK (
status IN ('pending', 'accepted', 'declined', 'revoked')
),
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
responded_at INTEGER
)
''');
await customStatement('''
INSERT INTO share_inbox_items_v23 (
share_id,
sender_user_id,
resource_type,
payload_json,
status,
created_at,
updated_at,
responded_at
)
SELECT
share_id,
sender_user_id,
resource_type,
payload_json,
status,
created_at,
updated_at,
responded_at
FROM share_inbox_items
''');
await customStatement('DROP TABLE share_inbox_items');
await customStatement(
'ALTER TABLE share_inbox_items_v23 RENAME TO share_inbox_items',
);
await customStatement('''
CREATE TABLE IF NOT EXISTS pending_share_actions_v23 (
id TEXT NOT NULL PRIMARY KEY,
action_type TEXT NOT NULL CHECK (
action_type IN ('send', 'accept', 'decline', 'revoke')
),
share_id TEXT,
resource_type TEXT CHECK (
resource_type IS NULL OR
resource_type IN ('program', 'workoutTemplate', 'pack')
),
payload_json TEXT,
recipient_emails_json TEXT,
created_at INTEGER NOT NULL,
last_attempt_at INTEGER,
attempt_count INTEGER NOT NULL CHECK (attempt_count >= 0),
status TEXT NOT NULL CHECK (status IN ('pending', 'succeeded', 'failed'))
)
''');
await customStatement('''
INSERT INTO pending_share_actions_v23 (
id,
action_type,
share_id,
resource_type,
payload_json,
recipient_emails_json,
created_at,
last_attempt_at,
attempt_count,
status
)
SELECT
id,
action_type,
share_id,
resource_type,
payload_json,
recipient_emails_json,
created_at,
last_attempt_at,
attempt_count,
status
FROM pending_share_actions
''');
await customStatement('DROP TABLE pending_share_actions');
await customStatement(
'ALTER TABLE pending_share_actions_v23 RENAME TO pending_share_actions',
);
}
Future<void> _migrateToSchema24(Migrator migrator) async {
await _addColumnIfMissing(
tableName: 'workout_history',
columnName: 'min_heart_rate_bpm',
definition:
'min_heart_rate_bpm INTEGER CHECK '
'(min_heart_rate_bpm IS NULL OR min_heart_rate_bpm > 0)',
);
await _addColumnIfMissing(
tableName: 'workout_history',
columnName: 'total_distance_meters',
definition:
'total_distance_meters REAL CHECK '
'(total_distance_meters IS NULL OR total_distance_meters >= 0)',
);
await _addColumnIfMissing(
tableName: 'workout_history',
columnName: 'total_calories_kcal',
definition:
'total_calories_kcal REAL CHECK '
'(total_calories_kcal IS NULL OR total_calories_kcal >= 0)',
);
await migrator.createTable(workoutTelemetrySamples);
await migrator.createTable(workoutTelemetryAggregates);
}
Future<void> _backfillWorkoutHistorySetSourceExerciseIds() async {
await customStatement(r'''
UPDATE workout_history_set_results AS result
@ -890,4 +1030,12 @@ WHERE result.source_exercise_id_snapshot IS NULL
final rows = await customSelect('PRAGMA table_info($tableName)').get();
return rows.any((row) => row.data['name'] == columnName);
}
Future<bool> _hasTable(String tableName) async {
final rows = await customSelect(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?",
variables: [Variable<String>(tableName)],
).get();
return rows.isNotEmpty;
}
}