// EGYETLEN track-szinkron útvonal: // 1. Élő pozíció — minden 3 mp-ben UPSERT → terepi_seged_device_positions // (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 'package:connectivity_plus/connectivity_plus.dart'; import 'package:get/get.dart'; import 'package:latlong2/latlong.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'ts_sync_service.dart'; import 'device_identity_service.dart'; import '../models/track.dart'; class TrackSyncService extends GetxService { static TrackSyncService get to => Get.find(); final _supabase = Supabase.instance.client; // ── Konfiguráció ────────────────────────────────────────────────── static const _batchSize = 10; static const _batchIntervalSec = 8; static const _positionIntervalSec = 3; // ── Belső állapot ───────────────────────────────────────────────── int _pointsSinceLastPush = 0; Timer? _pushTimer; Timer? _posTimer; Track? _track; LatLng? _lastPos; // utoljára kapott pozíció (broadcasthoz) bool _online = false; // ── Publikus állapot ────────────────────────────────────────────── final isSyncing = false.obs; // ── Lifecycle ───────────────────────────────────────────────────── @override Future onInit() async { super.onInit(); _online = await _checkOnline(); _listenConnectivity(); } @override void onClose() { _stopTimers(); super.onClose(); } // ── Session vezérlés (TrackingController hívja) ─────────────────── void startSession(Track track) { _track = track; _pointsSinceLastPush = 0; if (track.isLocalOnly) return; // 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), (_) => _pushTrackData(), ); _posTimer = Timer.periodic( const Duration(seconds: _positionIntervalSec), (_) => _broadcastPosition(), ); } Future stopSession(Track track) async { _stopTimers(); if (!track.isLocalOnly) { // Záró push: a lezárt állapotot (vég idő, státusz, végleges táv/ // pontszám) a TrackingController.stopRecording() már beírta // helyileg 'pending' státusszal — ez küldi fel + az utolsó pontokat. await _pushTrackData(); } await _setInactive(); _track = null; _lastPos = null; } // ── Pont pufferelés ─────────────────────────────────────────────── /// 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) { _lastPos = LatLng(point.latitude, point.longitude); if (_track == null || _track!.isLocalOnly) return; _pointsSinceLastPush++; if (_pointsSinceLastPush >= _batchSize) _pushTrackData(); } // ── 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ó — // pontosan az fut, amit a háttér-szinkron is futtatna, csak sűrűbben. Future _pushTrackData() async { if (!_online) return; _pointsSinceLastPush = 0; try { isSyncing.value = true; // Fejléc ELŐBB (FK: track_points.track_id → tracks.id), utána a // pontok — ugyanaz a sorrend, mint a háttér-szinkron ciklusában. await TsSyncService.to.pushTracks(); await TsSyncService.to.pushTrackPoints(); } catch (_) { // Hiba esetén a sorok 'pending' állapotban maradnak — a // következő ciklus (vagy a háttér-szinkron) újrapróbálja. } finally { isSyncing.value = false; } } // ── É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 _broadcastPosition() async { final pos = _lastPos; if (pos == null || !_online) return; final device = DeviceIdentityService.to; try { await _supabase.from('terepi_seged_device_positions').upsert({ 'device_id': device.deviceId, 'app_instance_id': device.appInstanceId, 'user_name': device.deviceLabel.value, 'latitude': pos.latitude, 'longitude': pos.longitude, 'track_id': _track?.uuid, 'is_active': true, 'updated_at': DateTime.now().toUtc().toIso8601String(), }, onConflict: 'device_id'); } catch (_) {} } Future _setInactive() async { final deviceId = DeviceIdentityService.to.deviceId; try { await _supabase.from('terepi_seged_device_positions').update( {'is_active': false, 'track_id': null}).eq('device_id', deviceId); } catch (_) {} } // ── Kapcsolat figyelés ──────────────────────────────────────────── void _listenConnectivity() { Connectivity().onConnectivityChanged.listen((results) async { final wasOffline = !_online; _online = results.any((r) => r != ConnectivityResult.none); if (wasOffline && _online) await _pushTrackData(); }); } Future _checkOnline() async { final r = await Connectivity().checkConnectivity(); return r.any((r) => r != ConnectivityResult.none); } void _stopTimers() { _pushTimer?.cancel(); _posTimer?.cancel(); _pushTimer = null; _posTimer = null; } }