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:
parent
60d051ee7b
commit
09009fee4c
3 changed files with 57 additions and 16 deletions
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue