Prepare secure config and reliable workout sync

This commit is contained in:
Hamza-Ayed
2026-10-04 00:33:44 +03:00
parent 05f1c9ec6b
commit e7f3c777d1
17 changed files with 553 additions and 119 deletions
+118 -25
View File
@@ -10,16 +10,15 @@ import '../models/workout_segment.dart';
/// تدير كل جداول SQLite: الإحداثيات، التمارين، المقاطع، طابور المزامنة
class DatabaseService extends GetxService {
static const String _dbName = 'sportpath_tracker.db';
static const int _dbVersion = 2;
static const int _dbVersion = 3;
late Database _db;
Database get db => _db;
@override
Future<void> onInit() async {
super.onInit();
Future<DatabaseService> init() async {
await _initDatabase();
return this;
}
Future<void> _initDatabase() async {
@@ -28,6 +27,9 @@ class DatabaseService extends GetxService {
_db = await openDatabase(
dbPath,
version: _dbVersion,
onConfigure: (db) async {
await db.execute('PRAGMA foreign_keys = ON');
},
onCreate: _onCreate,
onUpgrade: _onUpgrade,
);
@@ -136,6 +138,9 @@ class DatabaseService extends GetxService {
await db.execute(
'CREATE INDEX idx_sync_status ON sync_queue(status)',
);
await db.execute(
'CREATE INDEX idx_sync_workout ON sync_queue(workout_id)',
);
debugPrint('[DB] All tables created successfully');
}
@@ -155,6 +160,11 @@ class DatabaseService extends GetxService {
// Columns may already exist
}
}
if (oldVersion < 3) {
await db.execute(
'CREATE INDEX IF NOT EXISTS idx_sync_workout ON sync_queue(workout_id)',
);
}
}
// ─── GPS Coordinates CRUD ─────────────────────────────────────
@@ -234,6 +244,73 @@ class DatabaseService extends GetxService {
);
}
/// Atomically persist a finished workout, its segments, and its sync outbox item.
Future<void> saveCompletedWorkout(Workout workout, String payload) async {
await _db.transaction((txn) async {
final saved = await txn.query(
'workouts',
columns: ['id'],
where: 'id = ?',
whereArgs: [workout.id],
limit: 1,
);
if (saved.isEmpty) {
await txn.insert('workouts', workout.toMap());
} else {
final existingWorkout = await txn.query(
'workouts',
columns: ['sync_status', 'synced_at'],
where: 'id = ?',
whereArgs: [workout.id],
limit: 1,
);
final values = workout.toMap()
..['sync_status'] = existingWorkout.first['sync_status']
..['synced_at'] = existingWorkout.first['synced_at'];
await txn.update(
'workouts',
values,
where: 'id = ?',
whereArgs: [workout.id],
);
}
await txn.delete(
'workout_segments',
where: 'workout_id = ?',
whereArgs: [workout.id],
);
for (final segment in workout.segments) {
final values = segment.toMap()..['workout_id'] = workout.id;
await txn.insert('workout_segments', values);
}
final queued = await txn.query(
'sync_queue',
columns: ['id', 'status'],
where: 'workout_id = ?',
whereArgs: [workout.id],
orderBy: 'id ASC',
limit: 1,
);
if (queued.isEmpty) {
await txn.insert('sync_queue', {
'workout_id': workout.id,
'payload': payload,
'status': 'queued',
});
} else if (queued.first['status'] != 'sent') {
await txn.update(
'sync_queue',
{'payload': payload},
where: 'id = ?',
whereArgs: [queued.first['id']],
);
}
});
}
/// Update workout fields
Future<void> updateWorkout(Workout workout) async {
await _db.update(
@@ -369,33 +446,49 @@ class DatabaseService extends GetxService {
);
}
Future<int> getPendingSyncCount() async {
final result = await _db.rawQuery(
"SELECT COUNT(*) AS cnt FROM sync_queue WHERE status IN ('queued', 'failed')",
);
return Sqflite.firstIntValue(result) ?? 0;
}
Future<void> retryFailedSync(String workoutId) async {
await _db.update(
'sync_queue',
{
'status': 'queued',
'retry_count': 0,
'last_attempt': null,
'error_message': null,
},
where: "workout_id = ? AND status = 'failed'",
whereArgs: [workoutId],
);
}
/// Update sync queue item status
Future<void> updateSyncQueueItem(
int id,
String status, {
String? errorMessage,
}) async {
int newRetryCount = 0;
// سحبنا المنطق خارج الـ Map ليكون واضحاً ومقروءاً
if (status == 'failed') {
final currentItem =
await _db.query('sync_queue', where: 'id = ?', whereArgs: [id]);
if (currentItem.isNotEmpty) {
newRetryCount = (currentItem.first['retry_count'] as int? ?? 0) + 1;
}
}
await _db.update(
'sync_queue',
{
'status': status,
'last_attempt': DateTime.now().toIso8601String(),
'retry_count': newRetryCount, // وضعنا القيمة الجاهزة هنا
if (errorMessage != null) 'error_message': errorMessage,
},
where: 'id = ?',
whereArgs: [id],
await _db.rawUpdate(
'''
UPDATE sync_queue
SET status = ?,
last_attempt = ?,
retry_count = retry_count + CASE WHEN ? = 'failed' THEN 1 ELSE 0 END,
error_message = ?
WHERE id = ?
''',
[
status,
DateTime.now().toUtc().toIso8601String(),
status,
errorMessage,
id,
],
);
}