diff --git a/android/app/src/main/kotlin/dev/fiatcode/tracpulse/network/TraccarHttpClient.kt b/android/app/src/main/kotlin/dev/fiatcode/tracpulse/network/TraccarHttpClient.kt index 291db15..f6f5c39 100644 --- a/android/app/src/main/kotlin/dev/fiatcode/tracpulse/network/TraccarHttpClient.kt +++ b/android/app/src/main/kotlin/dev/fiatcode/tracpulse/network/TraccarHttpClient.kt @@ -17,8 +17,9 @@ data class TraccarConfig( val password: String = "" ) -class TraccarHttpClient { - +class TraccarHttpClient( + private val onLog: ((eventType: String, message: String) -> Unit)? = null +) { private val client = OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) @@ -28,15 +29,19 @@ class TraccarHttpClient { suspend fun sendLocation(config: TraccarConfig, location: Location): Result = withContext(Dispatchers.IO) { try { val url = buildLocationUrl(config, location) + onLog?.invoke("DEBUG", "HTTP → $url") val request = buildRequest(config, url) val response = client.newCall(request).execute() if (response.isSuccessful) { Result.success(Unit) } else { + val errorBody = response.body?.string() ?: "empty" + onLog?.invoke("ERROR", "HTTP ${response.code}: ${response.message} | $errorBody") Result.failure(Exception("HTTP ${response.code}: ${response.message}")) } } catch (e: Exception) { + onLog?.invoke("ERROR", "Network error: ${e.message}") Result.failure(e) } } @@ -49,6 +54,7 @@ class TraccarHttpClient { successCount++ } } + onLog?.invoke("SYNC", "Sent $successCount/${locations.size} locations") Result.success(successCount) } diff --git a/android/app/src/main/kotlin/dev/fiatcode/tracpulse/service/LocationTrackingService.kt b/android/app/src/main/kotlin/dev/fiatcode/tracpulse/service/LocationTrackingService.kt index 2c67e92..b7f690f 100644 --- a/android/app/src/main/kotlin/dev/fiatcode/tracpulse/service/LocationTrackingService.kt +++ b/android/app/src/main/kotlin/dev/fiatcode/tracpulse/service/LocationTrackingService.kt @@ -75,7 +75,7 @@ class LocationTrackingService : Service() { distanceFilterProcessor = DistanceFilterProcessor() heartbeatScheduler = HeartbeatScheduler(this) database = AppDatabase.getInstance(this) - httpClient = TraccarHttpClient() + httpClient = TraccarHttpClient { eventType, message -> logEvent(eventType, message) } connectivityReceiver = ConnectivityReceiver { online -> isNetworkAvailable = online logEvent("NETWORK_CHANGE", "Network: ${if (online) "online" else "offline"}") @@ -434,11 +434,13 @@ class LocationTrackingService : Service() { ) } - val result = httpClient.sendLocations(traccarConfig, locations) - if (result.isSuccess) { - val ids = unsyncedLocations.map { it.id } + val successCount = httpClient.sendLocations(traccarConfig, locations).getOrNull() ?: 0 + if (successCount > 0) { + val ids = unsyncedLocations.take(successCount).map { it.id } database.locationDao().markAllAsSynced(ids) - logEvent("SYNC", "Synced ${result.getOrNull()} buffered locations") + logEvent("SYNC", "Synced $successCount buffered locations") + } else { + logEvent("ERROR", "Failed to sync any of ${unsyncedLocations.size} buffered locations") } } } diff --git a/lib/status_screen.dart b/lib/status_screen.dart index 8793116..cf93305 100644 --- a/lib/status_screen.dart +++ b/lib/status_screen.dart @@ -13,7 +13,7 @@ class StatusScreen extends StatefulWidget { class _StatusScreenState extends State { List> _logs = []; final ScrollController _scrollController = ScrollController(); - String _filter = 'ALL'; + String _filter = 'NORMAL'; @override void initState() { @@ -107,6 +107,8 @@ class _StatusScreenState extends State { return const Color(0xFFff5252); case 'WARNING': return const Color(0xFFffb74d); + case 'INFO': + return const Color(0xFF00bcd4); case 'LOCATION': return const Color(0xFF69f0ae); case 'SYNC': @@ -115,6 +117,12 @@ class _StatusScreenState extends State { return const Color(0xFFffe082); case 'FILTERED': return const Color(0xFF546e7a); + case 'NETWORK_CHANGE': + return const Color(0xFFce93d8); + case 'DEBUG': + return const Color(0xFF78909c); + case 'SPEED_CALC': + return const Color(0xFF90a4ae); default: return const Color(0xFF78909c); } @@ -126,6 +134,8 @@ class _StatusScreenState extends State { return Icons.close; case 'WARNING': return Icons.warning; + case 'INFO': + return Icons.info; case 'LOCATION': return Icons.gps_fixed; case 'SYNC': @@ -134,13 +144,34 @@ class _StatusScreenState extends State { return Icons.favorite; case 'FILTERED': return Icons.filter_alt; + case 'NETWORK_CHANGE': + return Icons.signal_wifi_connected_no_internet_4; + case 'DEBUG': + return Icons.bug_report; + case 'SPEED_CALC': + return Icons.speed; default: return Icons.info; } } + static const _normalEventTypes = { + 'INFO', + 'LOCATION', + 'SYNC', + 'ERROR', + 'NETWORK_CHANGE', + 'HEARTBEAT', + 'WARNING', + }; + List> get _filteredLogs { - if (_filter == 'ALL') return _logs; + if (_filter == 'VERBOSE') return _logs; + if (_filter == 'NORMAL') { + return _logs + .where((l) => _normalEventTypes.contains(l['eventType'])) + .toList(); + } return _logs.where((l) => l['eventType'] == _filter).toList(); } @@ -207,13 +238,15 @@ class _StatusScreenState extends State { ), child: Row( children: [ - _buildFilterChip('ALL', null), + _buildFilterChip('NORMAL', 'NORMAL'), const SizedBox(width: 8), - _buildFilterChip('LOG', 'LOCATION'), + _buildFilterChip('LOCATION', 'LOCATION'), const SizedBox(width: 8), _buildFilterChip('SYNC', 'SYNC'), const SizedBox(width: 8), _buildFilterChip('ERR', 'ERROR'), + const SizedBox(width: 8), + _buildFilterChip('DEBUG', 'DEBUG'), ], ), ), @@ -231,7 +264,7 @@ class _StatusScreenState extends State { ), const SizedBox(height: 16), Text( - _filter == 'ALL' + _filter == 'VERBOSE' ? 'No events recorded' : 'No $_filter events', style: const TextStyle( @@ -268,10 +301,10 @@ class _StatusScreenState extends State { ); } - Widget _buildFilterChip(String label, String? eventType) { - final isSelected = _filter == (eventType ?? 'ALL'); + Widget _buildFilterChip(String label, String eventType) { + final isSelected = _filter == eventType; return GestureDetector( - onTap: () => setState(() => _filter = eventType ?? 'ALL'), + onTap: () => setState(() => _filter = eventType), child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( @@ -362,7 +395,7 @@ class _StatusScreenState extends State { ), const SizedBox(height: 5), // Line 2: message - Text( + SelectableText( msg, style: const TextStyle( fontFamily: 'monospace',