feat: improve logging and event filtering

- Add HTTP debug logging to TraccarHttpClient (URL, error body)
- Route HTTP client logs through app event log system
- Fix syncBufferedLocations to properly report partial failures
- Add DEBUG event type with separate filter chip
- Rename ALL filter to NORMAL, add VERBOSE (shows DEBUG, FILTERED, SPEED_CALC)
- Add color/icon for INFO, NETWORK_CHANGE, DEBUG, SPEED_CALC events
- Make log message text selectable
This commit is contained in:
fiatcode 2026-05-04 09:43:01 +07:00
parent 60d051ee7b
commit 09009fee4c
No known key found for this signature in database
3 changed files with 57 additions and 16 deletions

View file

@ -17,8 +17,9 @@ data class TraccarConfig(
val password: String = "" val password: String = ""
) )
class TraccarHttpClient { class TraccarHttpClient(
private val onLog: ((eventType: String, message: String) -> Unit)? = null
) {
private val client = OkHttpClient.Builder() private val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS)
@ -28,15 +29,19 @@ class TraccarHttpClient {
suspend fun sendLocation(config: TraccarConfig, location: Location): Result<Unit> = withContext(Dispatchers.IO) { suspend fun sendLocation(config: TraccarConfig, location: Location): Result<Unit> = withContext(Dispatchers.IO) {
try { try {
val url = buildLocationUrl(config, location) val url = buildLocationUrl(config, location)
onLog?.invoke("DEBUG", "HTTP → $url")
val request = buildRequest(config, url) val request = buildRequest(config, url)
val response = client.newCall(request).execute() val response = client.newCall(request).execute()
if (response.isSuccessful) { if (response.isSuccessful) {
Result.success(Unit) Result.success(Unit)
} else { } else {
val errorBody = response.body?.string() ?: "empty"
onLog?.invoke("ERROR", "HTTP ${response.code}: ${response.message} | $errorBody")
Result.failure(Exception("HTTP ${response.code}: ${response.message}")) Result.failure(Exception("HTTP ${response.code}: ${response.message}"))
} }
} catch (e: Exception) { } catch (e: Exception) {
onLog?.invoke("ERROR", "Network error: ${e.message}")
Result.failure(e) Result.failure(e)
} }
} }
@ -49,6 +54,7 @@ class TraccarHttpClient {
successCount++ successCount++
} }
} }
onLog?.invoke("SYNC", "Sent $successCount/${locations.size} locations")
Result.success(successCount) Result.success(successCount)
} }

View file

@ -75,7 +75,7 @@ class LocationTrackingService : Service() {
distanceFilterProcessor = DistanceFilterProcessor() distanceFilterProcessor = DistanceFilterProcessor()
heartbeatScheduler = HeartbeatScheduler(this) heartbeatScheduler = HeartbeatScheduler(this)
database = AppDatabase.getInstance(this) database = AppDatabase.getInstance(this)
httpClient = TraccarHttpClient() httpClient = TraccarHttpClient { eventType, message -> logEvent(eventType, message) }
connectivityReceiver = ConnectivityReceiver { online -> connectivityReceiver = ConnectivityReceiver { online ->
isNetworkAvailable = online isNetworkAvailable = online
logEvent("NETWORK_CHANGE", "Network: ${if (online) "online" else "offline"}") logEvent("NETWORK_CHANGE", "Network: ${if (online) "online" else "offline"}")
@ -434,11 +434,13 @@ class LocationTrackingService : Service() {
) )
} }
val result = httpClient.sendLocations(traccarConfig, locations) val successCount = httpClient.sendLocations(traccarConfig, locations).getOrNull() ?: 0
if (result.isSuccess) { if (successCount > 0) {
val ids = unsyncedLocations.map { it.id } val ids = unsyncedLocations.take(successCount).map { it.id }
database.locationDao().markAllAsSynced(ids) 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")
} }
} }
} }

View file

@ -13,7 +13,7 @@ class StatusScreen extends StatefulWidget {
class _StatusScreenState extends State<StatusScreen> { class _StatusScreenState extends State<StatusScreen> {
List<Map<String, dynamic>> _logs = []; List<Map<String, dynamic>> _logs = [];
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
String _filter = 'ALL'; String _filter = 'NORMAL';
@override @override
void initState() { void initState() {
@ -107,6 +107,8 @@ class _StatusScreenState extends State<StatusScreen> {
return const Color(0xFFff5252); return const Color(0xFFff5252);
case 'WARNING': case 'WARNING':
return const Color(0xFFffb74d); return const Color(0xFFffb74d);
case 'INFO':
return const Color(0xFF00bcd4);
case 'LOCATION': case 'LOCATION':
return const Color(0xFF69f0ae); return const Color(0xFF69f0ae);
case 'SYNC': case 'SYNC':
@ -115,6 +117,12 @@ class _StatusScreenState extends State<StatusScreen> {
return const Color(0xFFffe082); return const Color(0xFFffe082);
case 'FILTERED': case 'FILTERED':
return const Color(0xFF546e7a); 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: default:
return const Color(0xFF78909c); return const Color(0xFF78909c);
} }
@ -126,6 +134,8 @@ class _StatusScreenState extends State<StatusScreen> {
return Icons.close; return Icons.close;
case 'WARNING': case 'WARNING':
return Icons.warning; return Icons.warning;
case 'INFO':
return Icons.info;
case 'LOCATION': case 'LOCATION':
return Icons.gps_fixed; return Icons.gps_fixed;
case 'SYNC': case 'SYNC':
@ -134,13 +144,34 @@ class _StatusScreenState extends State<StatusScreen> {
return Icons.favorite; return Icons.favorite;
case 'FILTERED': case 'FILTERED':
return Icons.filter_alt; 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: default:
return Icons.info; return Icons.info;
} }
} }
static const _normalEventTypes = {
'INFO',
'LOCATION',
'SYNC',
'ERROR',
'NETWORK_CHANGE',
'HEARTBEAT',
'WARNING',
};
List<Map<String, dynamic>> get _filteredLogs { List<Map<String, dynamic>> 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(); return _logs.where((l) => l['eventType'] == _filter).toList();
} }
@ -207,13 +238,15 @@ class _StatusScreenState extends State<StatusScreen> {
), ),
child: Row( child: Row(
children: [ children: [
_buildFilterChip('ALL', null), _buildFilterChip('NORMAL', 'NORMAL'),
const SizedBox(width: 8), const SizedBox(width: 8),
_buildFilterChip('LOG', 'LOCATION'), _buildFilterChip('LOCATION', 'LOCATION'),
const SizedBox(width: 8), const SizedBox(width: 8),
_buildFilterChip('SYNC', 'SYNC'), _buildFilterChip('SYNC', 'SYNC'),
const SizedBox(width: 8), const SizedBox(width: 8),
_buildFilterChip('ERR', 'ERROR'), _buildFilterChip('ERR', 'ERROR'),
const SizedBox(width: 8),
_buildFilterChip('DEBUG', 'DEBUG'),
], ],
), ),
), ),
@ -231,7 +264,7 @@ class _StatusScreenState extends State<StatusScreen> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
_filter == 'ALL' _filter == 'VERBOSE'
? 'No events recorded' ? 'No events recorded'
: 'No $_filter events', : 'No $_filter events',
style: const TextStyle( style: const TextStyle(
@ -268,10 +301,10 @@ class _StatusScreenState extends State<StatusScreen> {
); );
} }
Widget _buildFilterChip(String label, String? eventType) { Widget _buildFilterChip(String label, String eventType) {
final isSelected = _filter == (eventType ?? 'ALL'); final isSelected = _filter == eventType;
return GestureDetector( return GestureDetector(
onTap: () => setState(() => _filter = eventType ?? 'ALL'), onTap: () => setState(() => _filter = eventType),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -362,7 +395,7 @@ class _StatusScreenState extends State<StatusScreen> {
), ),
const SizedBox(height: 5), const SizedBox(height: 5),
// Line 2: message // Line 2: message
Text( SelectableText(
msg, msg,
style: const TextStyle( style: const TextStyle(
fontFamily: 'monospace', fontFamily: 'monospace',