Track online szinkronizáció refraktorálása
This commit is contained in:
@@ -61,6 +61,7 @@ enum TrackSyncStatus { pending, synced, error }
|
|||||||
|
|
||||||
class Track {
|
class Track {
|
||||||
final int? id;
|
final int? id;
|
||||||
|
final String? uuid; // kliens-generált uuid — ez a szerveroldali id is
|
||||||
final int? projectId;
|
final int? projectId;
|
||||||
final String name;
|
final String name;
|
||||||
final DateTime startTime;
|
final DateTime startTime;
|
||||||
@@ -77,6 +78,7 @@ class Track {
|
|||||||
|
|
||||||
const Track(
|
const Track(
|
||||||
{this.id,
|
{this.id,
|
||||||
|
this.uuid,
|
||||||
this.projectId,
|
this.projectId,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.startTime,
|
required this.startTime,
|
||||||
@@ -91,6 +93,7 @@ class Track {
|
|||||||
|
|
||||||
Track copyWith(
|
Track copyWith(
|
||||||
{int? id,
|
{int? id,
|
||||||
|
String? uuid,
|
||||||
int? projectId,
|
int? projectId,
|
||||||
String? name,
|
String? name,
|
||||||
DateTime? startTime,
|
DateTime? startTime,
|
||||||
@@ -104,6 +107,7 @@ class Track {
|
|||||||
String? supabaseId}) =>
|
String? supabaseId}) =>
|
||||||
Track(
|
Track(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
|
uuid: uuid ?? this.uuid,
|
||||||
projectId: projectId ?? this.projectId,
|
projectId: projectId ?? this.projectId,
|
||||||
name: name ?? this.name,
|
name: name ?? this.name,
|
||||||
startTime: startTime ?? this.startTime,
|
startTime: startTime ?? this.startTime,
|
||||||
@@ -136,6 +140,7 @@ class Track {
|
|||||||
|
|
||||||
Map<String, dynamic> toMap() => {
|
Map<String, dynamic> toMap() => {
|
||||||
if (id != null) 'id': id,
|
if (id != null) 'id': id,
|
||||||
|
if (uuid != null) 'uuid': uuid,
|
||||||
if (projectId != null) 'project_id': projectId,
|
if (projectId != null) 'project_id': projectId,
|
||||||
'name': name,
|
'name': name,
|
||||||
'start_time': startTime.toIso8601String(),
|
'start_time': startTime.toIso8601String(),
|
||||||
@@ -151,6 +156,7 @@ class Track {
|
|||||||
|
|
||||||
factory Track.fromMap(Map<String, dynamic> m) => Track(
|
factory Track.fromMap(Map<String, dynamic> m) => Track(
|
||||||
id: m['id'] as int?,
|
id: m['id'] as int?,
|
||||||
|
uuid: m['uuid'] as String?,
|
||||||
projectId: m['project_id'] as int?,
|
projectId: m['project_id'] as int?,
|
||||||
name: m['name'] as String,
|
name: m['name'] as String,
|
||||||
startTime: DateTime.parse(m['start_time'] as String),
|
startTime: DateTime.parse(m['start_time'] as String),
|
||||||
|
|||||||
@@ -102,6 +102,16 @@ class TrackingController extends GetxController {
|
|||||||
super.onInit();
|
super.onInit();
|
||||||
_initForegroundTask();
|
_initForegroundTask();
|
||||||
loadSavedTracks();
|
loadSavedTracks();
|
||||||
|
|
||||||
|
// A TrackingController permanent, egyszer indul az app életciklusában —
|
||||||
|
// enélkül projektváltáskor a lista a réginél ragadna.
|
||||||
|
ever(ProjectService.to.activeProject, (_) {
|
||||||
|
loadSavedTracks();
|
||||||
|
// Ne maradjon a térképen egy MÁSIK projekt track-overlaya.
|
||||||
|
|
||||||
|
overlayTrackIds.clear();
|
||||||
|
_trackCoords.clear();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initForegroundTask() {
|
void _initForegroundTask() {
|
||||||
@@ -130,11 +140,18 @@ class TrackingController extends GetxController {
|
|||||||
|
|
||||||
/// Elindítja a rögzítést a megadott forrással (alapértelmezett: telefon GPS).
|
/// Elindítja a rögzítést a megadott forrással (alapértelmezett: telefon GPS).
|
||||||
Future<void> startRecording(
|
Future<void> startRecording(
|
||||||
{LocationSource? source,
|
{LocationSource? source, String? name, TrackSyncMode? syncMode}) async {
|
||||||
String? name,
|
|
||||||
TrackSyncMode syncMode = TrackSyncMode.online}) async {
|
|
||||||
if (isRecording.value) return;
|
if (isRecording.value) return;
|
||||||
|
|
||||||
|
// Alapértelmezés: a track kövesse az AKTÍV PROJEKT online/lokális
|
||||||
|
// állapotát — eddig ez mindig 'online' volt, függetlenül a projekt
|
||||||
|
// beállításától. Explicit paraméterrel felülírható.
|
||||||
|
final activeProject = ProjectService.to.activeProject.value;
|
||||||
|
final effectiveSyncMode = syncMode ??
|
||||||
|
(activeProject?.isLocalOnly ?? true
|
||||||
|
? TrackSyncMode.localOnly
|
||||||
|
: TrackSyncMode.online);
|
||||||
|
|
||||||
_source = source ?? PhoneGpsSource(intervalMs: 1000, distanceFilter: 2.0);
|
_source = source ?? PhoneGpsSource(intervalMs: 1000, distanceFilter: 2.0);
|
||||||
|
|
||||||
// Track létrehozása az adatbázisban
|
// Track létrehozása az adatbázisban
|
||||||
@@ -144,8 +161,8 @@ class TrackingController extends GetxController {
|
|||||||
name: trackName,
|
name: trackName,
|
||||||
startTime: now,
|
startTime: now,
|
||||||
source: _source!.displayName,
|
source: _source!.displayName,
|
||||||
projectId: ProjectService.to.activeProjectId,
|
projectId: activeProject?.id,
|
||||||
syncMode: syncMode));
|
syncMode: effectiveSyncMode));
|
||||||
currentTrack.value = await _db.getTrack(trackId);
|
currentTrack.value = await _db.getTrack(trackId);
|
||||||
|
|
||||||
// Állapot reset
|
// Állapot reset
|
||||||
@@ -162,20 +179,11 @@ class TrackingController extends GetxController {
|
|||||||
callback: startTrackingCallback,
|
callback: startTrackingCallback,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!(currentTrack.value?.isLocalOnly ?? true)) {
|
// A TrackSyncService.startSession() maga dönti el (a track
|
||||||
TrackSyncService.to
|
// isLocalOnly-ja alapján), hogy indítja-e a szinkron-időzítőket —
|
||||||
.createRemoteTrack(currentTrack.value!)
|
// nincs többé külön "előbb hozzuk létre szerveroldalon" lépés, mert
|
||||||
.then((supabaseId) async {
|
// az egységes (uuid-alapú) push maga hozza létre a sort a szerveren.
|
||||||
if (supabaseId == null) return;
|
TrackSyncService.to.startSession(currentTrack.value!);
|
||||||
final updated = currentTrack.value!.copyWith(supabaseId: supabaseId);
|
|
||||||
await _db.updateTrack(updated);
|
|
||||||
currentTrack.value = updated;
|
|
||||||
TrackSyncService.to.startSession(updated);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
TrackSyncService.to.startSession(currentTrack.value!);
|
|
||||||
}
|
|
||||||
|
|
||||||
// GPS stream feliratkozás
|
// GPS stream feliratkozás
|
||||||
_positionSub = _source!.positionStream.listen(
|
_positionSub = _source!.positionStream.listen(
|
||||||
_onPosition,
|
_onPosition,
|
||||||
@@ -260,7 +268,8 @@ class TrackingController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadSavedTracks() async {
|
Future<void> loadSavedTracks() async {
|
||||||
savedTracks.value = await _db.listTracks();
|
savedTracks.value =
|
||||||
|
await _db.listTracks(projectId: ProjectService.to.activeProjectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteTrack(int id) async {
|
Future<void> deleteTrack(int id) async {
|
||||||
|
|||||||
@@ -57,6 +57,14 @@ class AppDatabase {
|
|||||||
Future<void> upgradeSchema(Database db, int oldVersion, int newVersion) =>
|
Future<void> upgradeSchema(Database db, int oldVersion, int newVersion) =>
|
||||||
_onUpgrade(db, oldVersion, newVersion);
|
_onUpgrade(db, oldVersion, newVersion);
|
||||||
|
|
||||||
|
Future<void> _tryExec(Database db, String sql) async {
|
||||||
|
try {
|
||||||
|
await db.execute(sql);
|
||||||
|
} on DatabaseException catch (e) {
|
||||||
|
if (!e.toString().contains('duplicate column')) rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _onCreate(Database db, int _) async {
|
Future<void> _onCreate(Database db, int _) async {
|
||||||
// ── Projects ────────────────────────────────────────────────────
|
// ── Projects ────────────────────────────────────────────────────
|
||||||
// uuid: a projekt globális azonosítója. Közös projekthez csatlakozáskor
|
// uuid: a projekt globális azonosítója. Közös projekthez csatlakozáskor
|
||||||
@@ -261,6 +269,7 @@ class AppDatabase {
|
|||||||
|
|
||||||
await _createStakeoutTable(db);
|
await _createStakeoutTable(db);
|
||||||
await _createContactsOutbox(db);
|
await _createContactsOutbox(db);
|
||||||
|
await _addAppInstanceIdColumns(db);
|
||||||
|
|
||||||
// Alap projekt létrehozása az első indításhoz
|
// Alap projekt létrehozása az első indításhoz
|
||||||
final now = DateTime.now().toIso8601String();
|
final now = DateTime.now().toIso8601String();
|
||||||
@@ -296,6 +305,8 @@ class AppDatabase {
|
|||||||
if (oldVersion < 5) {
|
if (oldVersion < 5) {
|
||||||
_createContactsOutbox(db);
|
_createContactsOutbox(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await _addAppInstanceIdColumns(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _migrateToV4(Database db) async {
|
Future<void> _migrateToV4(Database db) async {
|
||||||
@@ -499,6 +510,7 @@ class AppDatabase {
|
|||||||
DeviceIdentityService.to.isReady
|
DeviceIdentityService.to.isReady
|
||||||
? DeviceIdentityService.to.appInstanceId
|
? DeviceIdentityService.to.appInstanceId
|
||||||
: null;
|
: null;
|
||||||
|
map['app_instance_id'] ??= DeviceIdentityService.to.appInstanceId;
|
||||||
return db.insert('tracks', map);
|
return db.insert('tracks', map);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,10 +536,14 @@ class AppDatabase {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Track>> listTracks() async {
|
Future<List<Track>> listTracks({int? projectId}) async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
final rows = await db.query('tracks',
|
final rows = await db.query('tracks',
|
||||||
where: 'deleted_at IS NULL', orderBy: 'start_time DESC');
|
where: projectId != null
|
||||||
|
? 'deleted_at IS NULL AND project_id = ?'
|
||||||
|
: 'deleted_at IS NULL',
|
||||||
|
whereArgs: projectId != null ? [projectId] : null,
|
||||||
|
orderBy: 'start_time DESC');
|
||||||
return rows.map(Track.fromMap).toList();
|
return rows.map(Track.fromMap).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -554,9 +570,11 @@ class AppDatabase {
|
|||||||
await txn.rawUpdate('''
|
await txn.rawUpdate('''
|
||||||
UPDATE tracks
|
UPDATE tracks
|
||||||
SET distance_m = ?,
|
SET distance_m = ?,
|
||||||
point_count = point_count + 1
|
point_count = point_count + 1,
|
||||||
|
updated_at = ?,
|
||||||
|
sync_status = 'pending'
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
''', [newDistance, point.trackId]);
|
''', [newDistance, DateTime.now().toIso8601String(), point.trackId]);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print(
|
print(
|
||||||
@@ -879,6 +897,8 @@ class AppDatabase {
|
|||||||
DeviceIdentityService.to.isReady
|
DeviceIdentityService.to.isReady
|
||||||
? DeviceIdentityService.to.appInstanceId
|
? DeviceIdentityService.to.appInstanceId
|
||||||
: null;
|
: null;
|
||||||
|
map['app_instance_id'] ??= DeviceIdentityService.to.appInstanceId;
|
||||||
|
|
||||||
return db.insert('measured_points', map);
|
return db.insert('measured_points', map);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -974,7 +994,7 @@ class AppDatabase {
|
|||||||
DeviceIdentityService.to.isReady
|
DeviceIdentityService.to.isReady
|
||||||
? DeviceIdentityService.to.appInstanceId
|
? DeviceIdentityService.to.appInstanceId
|
||||||
: null;
|
: null;
|
||||||
;
|
map['app_instance_id'] ??= DeviceIdentityService.to.appInstanceId;
|
||||||
return db.insert('stakeout_points', map);
|
return db.insert('stakeout_points', map);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -998,6 +1018,8 @@ class AppDatabase {
|
|||||||
DeviceIdentityService.to.isReady
|
DeviceIdentityService.to.isReady
|
||||||
? DeviceIdentityService.to.appInstanceId
|
? DeviceIdentityService.to.appInstanceId
|
||||||
: null;
|
: null;
|
||||||
|
map['app_instance_id'] ??= DeviceIdentityService.to.appInstanceId;
|
||||||
|
|
||||||
await txn.insert('stakeout_points', map);
|
await txn.insert('stakeout_points', map);
|
||||||
inserted++;
|
inserted++;
|
||||||
}
|
}
|
||||||
@@ -1353,6 +1375,16 @@ class AppDatabase {
|
|||||||
0;
|
0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A device_id (ANDROID_ID, TEXT) MARAD — helyes, mert az oszlop nem
|
||||||
|
/// uuid típusú. Az app_instance_id egy KIEGÉSZÍTŐ azonosító: az adott
|
||||||
|
/// app-telepítéshez tartozó, saját generált uuid — mindkettő tárolva,
|
||||||
|
/// hogy az "eszköz" és a "konkrét app-példány" is visszakereshető legyen.
|
||||||
|
Future<void> _addAppInstanceIdColumns(Database db) async {
|
||||||
|
for (final table in ['tracks', 'measured_points', 'stakeout_points']) {
|
||||||
|
await _tryExec(db, 'ALTER TABLE $table ADD COLUMN app_instance_id TEXT');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Future<void> testOnly() async {
|
// Future<void> testOnly() async {
|
||||||
// final db = await database;
|
// final db = await database;
|
||||||
// await db.execute(
|
// await db.execute(
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ class StakeoutSyncService extends GetxService {
|
|||||||
'offset_dist': r['offset_dist'],
|
'offset_dist': r['offset_dist'],
|
||||||
'offset_bearing': r['offset_bearing'],
|
'offset_bearing': r['offset_bearing'],
|
||||||
'device_id': r['device_id'],
|
'device_id': r['device_id'],
|
||||||
|
'app_instance_id': r['app_instance_id'],
|
||||||
'deleted_at': _toUtc(r['deleted_at'] as String?),
|
'deleted_at': _toUtc(r['deleted_at'] as String?),
|
||||||
})
|
})
|
||||||
.toList();
|
.toList();
|
||||||
@@ -164,6 +165,7 @@ class StakeoutSyncService extends GetxService {
|
|||||||
'offset_bearing': r['offset_bearing'],
|
'offset_bearing': r['offset_bearing'],
|
||||||
'created_by': r['created_by'],
|
'created_by': r['created_by'],
|
||||||
'device_id': r['device_id'],
|
'device_id': r['device_id'],
|
||||||
|
'app_instance_id': r['app_instance_id'],
|
||||||
'created_at': r['created_at'] ?? DateTime.now().toIso8601String(),
|
'created_at': r['created_at'] ?? DateTime.now().toIso8601String(),
|
||||||
'updated_at': r['updated_at'],
|
'updated_at': r['updated_at'],
|
||||||
'deleted_at': r['deleted_at'],
|
'deleted_at': r['deleted_at'],
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ class TrackDatabase {
|
|||||||
Future<int> insertTrack(Track t) => AppDatabase.instance.insertTrack(t);
|
Future<int> insertTrack(Track t) => AppDatabase.instance.insertTrack(t);
|
||||||
Future<void> updateTrack(Track t) => AppDatabase.instance.updateTrack(t);
|
Future<void> updateTrack(Track t) => AppDatabase.instance.updateTrack(t);
|
||||||
Future<void> deleteTrack(int id) => AppDatabase.instance.deleteTrack(id);
|
Future<void> deleteTrack(int id) => AppDatabase.instance.deleteTrack(id);
|
||||||
Future<List<Track>> listTracks() => AppDatabase.instance.listTracks();
|
Future<List<Track>> listTracks({int? projectId}) =>
|
||||||
|
AppDatabase.instance.listTracks(projectId: projectId);
|
||||||
Future<Track?> getTrack(int id) => AppDatabase.instance.getTrack(id);
|
Future<Track?> getTrack(int id) => AppDatabase.instance.getTrack(id);
|
||||||
Future<void> addPoint(TrackPoint p, double d) =>
|
Future<void> addPoint(TrackPoint p, double d) =>
|
||||||
AppDatabase.instance.addPoint(p, d);
|
AppDatabase.instance.addPoint(p, d);
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
// Kétszintű Supabase szinkronizáció:
|
// EGYETLEN track-szinkron útvonal:
|
||||||
// 1. Élő pozíció — minden 3 mp-ben UPSERT → device_positions
|
// 1. Élő pozíció — minden 3 mp-ben UPSERT → terepi_seged_device_positions
|
||||||
// 2. Track pontok — batch INSERT (10 pont vagy 8 mp) → terepi_track_points
|
// (ez a "hol van most ez az eszköz" jelenlét, független a track-adattól)
|
||||||
|
// 2. Track fejléc + pontok — a TsSyncService KÖZÖS, uuid-alapú, idempotens
|
||||||
|
// push-függvényeit hívja, csak SŰRŰBBEN (10 pont vagy 8 mp), amíg fut
|
||||||
|
// a rögzítés. Nincs külön insert/upsert-kód és külön szerveroldali
|
||||||
|
// azonosító — pontosan ugyanaz fut, mint amit a háttér-szinkron
|
||||||
|
// (TsSyncService) 3 percenként amúgy is lefuttatna.
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
@@ -9,7 +14,7 @@ import 'package:get/get.dart';
|
|||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
import 'app_database.dart';
|
import 'ts_sync_service.dart';
|
||||||
import 'device_identity_service.dart';
|
import 'device_identity_service.dart';
|
||||||
import '../models/track.dart';
|
import '../models/track.dart';
|
||||||
|
|
||||||
@@ -24,8 +29,8 @@ class TrackSyncService extends GetxService {
|
|||||||
static const _positionIntervalSec = 3;
|
static const _positionIntervalSec = 3;
|
||||||
|
|
||||||
// ── Belső állapot ─────────────────────────────────────────────────
|
// ── Belső állapot ─────────────────────────────────────────────────
|
||||||
final _buffer = <TrackPoint>[];
|
int _pointsSinceLastPush = 0;
|
||||||
Timer? _batchTimer;
|
Timer? _pushTimer;
|
||||||
Timer? _posTimer;
|
Timer? _posTimer;
|
||||||
|
|
||||||
Track? _track;
|
Track? _track;
|
||||||
@@ -34,7 +39,6 @@ class TrackSyncService extends GetxService {
|
|||||||
|
|
||||||
// ── Publikus állapot ──────────────────────────────────────────────
|
// ── Publikus állapot ──────────────────────────────────────────────
|
||||||
final isSyncing = false.obs;
|
final isSyncing = false.obs;
|
||||||
final pendingCount = 0.obs;
|
|
||||||
|
|
||||||
// ── Lifecycle ─────────────────────────────────────────────────────
|
// ── Lifecycle ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -55,14 +59,17 @@ class TrackSyncService extends GetxService {
|
|||||||
|
|
||||||
void startSession(Track track) {
|
void startSession(Track track) {
|
||||||
_track = track;
|
_track = track;
|
||||||
_buffer.clear();
|
_pointsSinceLastPush = 0;
|
||||||
pendingCount.value = 0;
|
|
||||||
|
|
||||||
if (track.isLocalOnly) return;
|
if (track.isLocalOnly) return;
|
||||||
|
|
||||||
_batchTimer = Timer.periodic(
|
// Azonnali első push — ne kelljen az első 8 mp-et kivárni ahhoz, hogy
|
||||||
|
// a track fejléce megjelenjen a szerveren más eszközök számára.
|
||||||
|
unawaited(_pushTrackData());
|
||||||
|
|
||||||
|
_pushTimer = Timer.periodic(
|
||||||
const Duration(seconds: _batchIntervalSec),
|
const Duration(seconds: _batchIntervalSec),
|
||||||
(_) => _flush(),
|
(_) => _pushTrackData(),
|
||||||
);
|
);
|
||||||
_posTimer = Timer.periodic(
|
_posTimer = Timer.periodic(
|
||||||
const Duration(seconds: _positionIntervalSec),
|
const Duration(seconds: _positionIntervalSec),
|
||||||
@@ -72,20 +79,12 @@ class TrackSyncService extends GetxService {
|
|||||||
|
|
||||||
Future<void> stopSession(Track track) async {
|
Future<void> stopSession(Track track) async {
|
||||||
_stopTimers();
|
_stopTimers();
|
||||||
if (track.isLocalOnly) return;
|
if (!track.isLocalOnly) {
|
||||||
|
// Záró push: a lezárt állapotot (vég idő, státusz, végleges táv/
|
||||||
await _flush(); // utolsó batch
|
// pontszám) a TrackingController.stopRecording() már beírta
|
||||||
|
// helyileg 'pending' státusszal — ez küldi fel + az utolsó pontokat.
|
||||||
// Track fejléc lezárása Supabase-ben
|
await _pushTrackData();
|
||||||
if (track.supabaseId != null) {
|
|
||||||
await _supabase.from('terepi_seged_tracks').update({
|
|
||||||
'end_time': track.endTime?.toIso8601String(),
|
|
||||||
'status': 'finished',
|
|
||||||
'distance_m': track.distanceMeters,
|
|
||||||
'point_count': track.pointCount,
|
|
||||||
}).eq('id', track.supabaseId!);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await _setInactive();
|
await _setInactive();
|
||||||
_track = null;
|
_track = null;
|
||||||
_lastPos = null;
|
_lastPos = null;
|
||||||
@@ -93,79 +92,42 @@ class TrackSyncService extends GetxService {
|
|||||||
|
|
||||||
// ── Pont pufferelés ───────────────────────────────────────────────
|
// ── Pont pufferelés ───────────────────────────────────────────────
|
||||||
|
|
||||||
/// TrackingController._onPosition() hívja minden pontnál
|
/// TrackingController._onPosition() hívja minden pontnál. A pont már
|
||||||
|
/// ekkor a LOKÁLIS adatbázisban van (a controller írja be) — itt csak
|
||||||
|
/// azt számoljuk, mikor induljon köztes push a 8 mp-es időzítő előtt.
|
||||||
void onNewPoint(TrackPoint point) {
|
void onNewPoint(TrackPoint point) {
|
||||||
_lastPos = LatLng(point.latitude, point.longitude);
|
_lastPos = LatLng(point.latitude, point.longitude);
|
||||||
|
|
||||||
if (_track == null || _track!.isLocalOnly) return;
|
if (_track == null || _track!.isLocalOnly) return;
|
||||||
|
|
||||||
_buffer.add(point);
|
_pointsSinceLastPush++;
|
||||||
pendingCount.value = _buffer.length;
|
if (_pointsSinceLastPush >= _batchSize) _pushTrackData();
|
||||||
|
|
||||||
if (_buffer.length >= _batchSize) _flush();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Supabase track létrehozása ────────────────────────────────────
|
// ── Push a KÖZÖS, uuid-alapú TsSyncService-függvényekkel ──────────
|
||||||
|
// Nincs saját insert/upsert-kód, nincs külön szerveroldali azonosító —
|
||||||
/// startRecording()-ban hívandó, visszaadja a Supabase UUID-t
|
// pontosan az fut, amit a háttér-szinkron is futtatna, csak sűrűbben.
|
||||||
Future<String?> createRemoteTrack(Track track) async {
|
|
||||||
if (!_online) return null;
|
|
||||||
try {
|
|
||||||
final res = await _supabase
|
|
||||||
.from('terepi_seged_tracks')
|
|
||||||
.insert({
|
|
||||||
'device_id': DeviceIdentityService.to.deviceId,
|
|
||||||
'name': track.name,
|
|
||||||
'source': track.source,
|
|
||||||
'start_time': track.startTime.toIso8601String(),
|
|
||||||
'status': 'recording',
|
|
||||||
})
|
|
||||||
.select('id')
|
|
||||||
.single();
|
|
||||||
return res['id'] as String?;
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Batch feltöltés ───────────────────────────────────────────────
|
|
||||||
|
|
||||||
Future<void> _flush() async {
|
|
||||||
if (_buffer.isEmpty || !_online) return;
|
|
||||||
|
|
||||||
final supabaseId = _track?.supabaseId;
|
|
||||||
if (supabaseId == null) return;
|
|
||||||
|
|
||||||
final batch = List<TrackPoint>.from(_buffer);
|
|
||||||
_buffer.clear();
|
|
||||||
pendingCount.value = 0;
|
|
||||||
|
|
||||||
|
Future<void> _pushTrackData() async {
|
||||||
|
if (!_online) return;
|
||||||
|
_pointsSinceLastPush = 0;
|
||||||
try {
|
try {
|
||||||
isSyncing.value = true;
|
isSyncing.value = true;
|
||||||
await _supabase.from('terepi_seged_track_points').insert(
|
// Fejléc ELŐBB (FK: track_points.track_id → tracks.id), utána a
|
||||||
batch
|
// pontok — ugyanaz a sorrend, mint a háttér-szinkron ciklusában.
|
||||||
.map((p) => {
|
await TsSyncService.to.pushTracks();
|
||||||
'track_id': supabaseId,
|
await TsSyncService.to.pushTrackPoints();
|
||||||
'latitude': p.latitude,
|
|
||||||
'longitude': p.longitude,
|
|
||||||
'altitude': p.altitude,
|
|
||||||
'accuracy': p.accuracy,
|
|
||||||
'speed': p.speed,
|
|
||||||
'heading': p.heading,
|
|
||||||
'timestamp': p.timestamp.toIso8601String(),
|
|
||||||
})
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// Hiba → visszateszi a bufferbe
|
// Hiba esetén a sorok 'pending' állapotban maradnak — a
|
||||||
_buffer.insertAll(0, batch);
|
// következő ciklus (vagy a háttér-szinkron) újrapróbálja.
|
||||||
pendingCount.value = _buffer.length;
|
|
||||||
} finally {
|
} finally {
|
||||||
isSyncing.value = false;
|
isSyncing.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Élő pozíció broadcast ─────────────────────────────────────────
|
// ── Élő pozíció broadcast ─────────────────────────────────────────
|
||||||
|
// Ez a "hol van most ez az eszköz" jelenlét — nem track-adat, ezért
|
||||||
|
// marad önálló, egyszerű upsert.
|
||||||
|
|
||||||
Future<void> _broadcastPosition() async {
|
Future<void> _broadcastPosition() async {
|
||||||
final pos = _lastPos;
|
final pos = _lastPos;
|
||||||
@@ -175,10 +137,11 @@ class TrackSyncService extends GetxService {
|
|||||||
try {
|
try {
|
||||||
await _supabase.from('terepi_seged_device_positions').upsert({
|
await _supabase.from('terepi_seged_device_positions').upsert({
|
||||||
'device_id': device.deviceId,
|
'device_id': device.deviceId,
|
||||||
|
'app_instance_id': device.appInstanceId,
|
||||||
'user_name': device.deviceLabel.value,
|
'user_name': device.deviceLabel.value,
|
||||||
'latitude': pos.latitude,
|
'latitude': pos.latitude,
|
||||||
'longitude': pos.longitude,
|
'longitude': pos.longitude,
|
||||||
'track_id': _track?.supabaseId,
|
'track_id': _track?.uuid,
|
||||||
'is_active': true,
|
'is_active': true,
|
||||||
'updated_at': DateTime.now().toUtc().toIso8601String(),
|
'updated_at': DateTime.now().toUtc().toIso8601String(),
|
||||||
}, onConflict: 'device_id');
|
}, onConflict: 'device_id');
|
||||||
@@ -193,53 +156,13 @@ class TrackSyncService extends GetxService {
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Offline → Online szinkron ─────────────────────────────────────
|
|
||||||
|
|
||||||
Future<void> syncTrack(Track track) async {
|
|
||||||
if (!_online) return;
|
|
||||||
|
|
||||||
String? supabaseId = track.supabaseId;
|
|
||||||
if (supabaseId == null) {
|
|
||||||
supabaseId = await createRemoteTrack(track);
|
|
||||||
if (supabaseId == null) return;
|
|
||||||
final updated = track.copyWith(supabaseId: supabaseId);
|
|
||||||
await AppDatabase.instance.updateTrack(updated);
|
|
||||||
}
|
|
||||||
|
|
||||||
final points = await AppDatabase.instance.getPoints(track.id!);
|
|
||||||
if (points.isEmpty) return;
|
|
||||||
|
|
||||||
const chunk = 100;
|
|
||||||
for (int i = 0; i < points.length; i += chunk) {
|
|
||||||
final slice = points.sublist(i, (i + chunk).clamp(0, points.length));
|
|
||||||
await _supabase.from('terepi_seged_track_points').insert(
|
|
||||||
slice
|
|
||||||
.map((p) => {
|
|
||||||
'track_id': supabaseId,
|
|
||||||
'latitude': p.latitude,
|
|
||||||
'longitude': p.longitude,
|
|
||||||
'altitude': p.altitude,
|
|
||||||
'accuracy': p.accuracy,
|
|
||||||
'speed': p.speed,
|
|
||||||
'heading': p.heading,
|
|
||||||
'timestamp': p.timestamp.toIso8601String(),
|
|
||||||
})
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await AppDatabase.instance.updateTrack(
|
|
||||||
track.copyWith(supabaseId: supabaseId),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Kapcsolat figyelés ────────────────────────────────────────────
|
// ── Kapcsolat figyelés ────────────────────────────────────────────
|
||||||
|
|
||||||
void _listenConnectivity() {
|
void _listenConnectivity() {
|
||||||
Connectivity().onConnectivityChanged.listen((results) async {
|
Connectivity().onConnectivityChanged.listen((results) async {
|
||||||
final wasOffline = !_online;
|
final wasOffline = !_online;
|
||||||
_online = results.any((r) => r != ConnectivityResult.none);
|
_online = results.any((r) => r != ConnectivityResult.none);
|
||||||
if (wasOffline && _online) await _flush();
|
if (wasOffline && _online) await _pushTrackData();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,9 +172,9 @@ class TrackSyncService extends GetxService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _stopTimers() {
|
void _stopTimers() {
|
||||||
_batchTimer?.cancel();
|
_pushTimer?.cancel();
|
||||||
_posTimer?.cancel();
|
_posTimer?.cancel();
|
||||||
_batchTimer = null;
|
_pushTimer = null;
|
||||||
_posTimer = null;
|
_posTimer = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,8 +141,8 @@ class TsSyncService extends GetxService {
|
|||||||
Future<void> _push() async {
|
Future<void> _push() async {
|
||||||
await _pushProjects();
|
await _pushProjects();
|
||||||
await _pushMeasuredPoints();
|
await _pushMeasuredPoints();
|
||||||
await _pushTracks();
|
await pushTracks();
|
||||||
await _pushTrackPoints();
|
await pushTrackPoints();
|
||||||
await _pushNoteItems();
|
await _pushNoteItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,6 +188,7 @@ class TsSyncService extends GetxService {
|
|||||||
'measured_at': _toUtc(r['timestamp'] as String?),
|
'measured_at': _toUtc(r['timestamp'] as String?),
|
||||||
'note': r['note'],
|
'note': r['note'],
|
||||||
'device_id': r['device_id'],
|
'device_id': r['device_id'],
|
||||||
|
'app_instance_id': r['app_instance_id'],
|
||||||
'deleted_at': _toUtc(r['deleted_at'] as String?),
|
'deleted_at': _toUtc(r['deleted_at'] as String?),
|
||||||
})
|
})
|
||||||
.toList();
|
.toList();
|
||||||
@@ -197,7 +198,7 @@ class TsSyncService extends GetxService {
|
|||||||
'measured_points', rows.map((r) => r['uuid'] as String).toList());
|
'measured_points', rows.map((r) => r['uuid'] as String).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _pushTracks() async {
|
Future<void> pushTracks() async {
|
||||||
final rows = await _db.pendingTracks();
|
final rows = await _db.pendingTracks();
|
||||||
if (rows.isEmpty) return;
|
if (rows.isEmpty) return;
|
||||||
|
|
||||||
@@ -213,6 +214,7 @@ class TsSyncService extends GetxService {
|
|||||||
'distance_m': r['distance_m'],
|
'distance_m': r['distance_m'],
|
||||||
'point_count': r['point_count'],
|
'point_count': r['point_count'],
|
||||||
'device_id': r['device_id'],
|
'device_id': r['device_id'],
|
||||||
|
'app_instance_id': r['app_instance_id'],
|
||||||
'deleted_at': _toUtc(r['deleted_at'] as String?),
|
'deleted_at': _toUtc(r['deleted_at'] as String?),
|
||||||
})
|
})
|
||||||
.toList();
|
.toList();
|
||||||
@@ -225,7 +227,7 @@ class TsSyncService extends GetxService {
|
|||||||
/// Track-pontok: nagy mennyiség lehet, ezért 500-as batch-ekben, ciklusban,
|
/// Track-pontok: nagy mennyiség lehet, ezért 500-as batch-ekben, ciklusban,
|
||||||
/// amíg el nem fogy. A szerveren nincs UPDATE policy (append-only), ezért
|
/// amíg el nem fogy. A szerveren nincs UPDATE policy (append-only), ezért
|
||||||
/// ignoreDuplicates — az ismételt feltöltés csendben kimarad.
|
/// ignoreDuplicates — az ismételt feltöltés csendben kimarad.
|
||||||
Future<void> _pushTrackPoints() async {
|
Future<void> pushTrackPoints() async {
|
||||||
while (true) {
|
while (true) {
|
||||||
final rows = await _db.pendingTrackPoints(limit: 500);
|
final rows = await _db.pendingTrackPoints(limit: 500);
|
||||||
if (rows.isEmpty) break;
|
if (rows.isEmpty) break;
|
||||||
@@ -328,6 +330,7 @@ class TsSyncService extends GetxService {
|
|||||||
'note': r['note'] ?? '',
|
'note': r['note'] ?? '',
|
||||||
'created_by': r['created_by'],
|
'created_by': r['created_by'],
|
||||||
'device_id': r['device_id'],
|
'device_id': r['device_id'],
|
||||||
|
'app_instance_id': r['app_instance_id'],
|
||||||
'updated_at': r['updated_at'],
|
'updated_at': r['updated_at'],
|
||||||
'deleted_at': r['deleted_at'],
|
'deleted_at': r['deleted_at'],
|
||||||
'sync_status': 'synced',
|
'sync_status': 'synced',
|
||||||
@@ -358,6 +361,7 @@ class TsSyncService extends GetxService {
|
|||||||
'is_local_only': 0,
|
'is_local_only': 0,
|
||||||
'created_by': r['created_by'],
|
'created_by': r['created_by'],
|
||||||
'device_id': r['device_id'],
|
'device_id': r['device_id'],
|
||||||
|
'app_instance_id': r['app_instance_id'],
|
||||||
'updated_at': r['updated_at'],
|
'updated_at': r['updated_at'],
|
||||||
'deleted_at': r['deleted_at'],
|
'deleted_at': r['deleted_at'],
|
||||||
'sync_status': 'synced',
|
'sync_status': 'synced',
|
||||||
|
|||||||
Reference in New Issue
Block a user