tracpulse/lib/status_screen.dart
fiatcode 09009fee4c
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
2026-05-04 09:43:01 +07:00

410 lines
12 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'bridge/location_bridge.dart';
class StatusScreen extends StatefulWidget {
const StatusScreen({super.key});
@override
State<StatusScreen> createState() => _StatusScreenState();
}
class _StatusScreenState extends State<StatusScreen> {
List<Map<String, dynamic>> _logs = [];
final ScrollController _scrollController = ScrollController();
String _filter = 'NORMAL';
@override
void initState() {
super.initState();
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarColor: Color(0xFF0d0d0d),
statusBarIconBrightness: Brightness.light,
),
);
_loadLogs();
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
Future<void> _loadLogs() async {
final logs = await LocationBridge.getLogs();
if (mounted) {
setState(() {
_logs = logs.reversed.toList();
});
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients) {
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
}
});
}
}
Future<void> _clearLogs() async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: const Color(0xFF1e1e1e),
title: const Text(
'Clear Event Logs',
style: TextStyle(fontFamily: 'monospace', color: Color(0xFFe0e0e0)),
),
content: const Text(
'This will permanently delete all logged events. Continue?',
style: TextStyle(fontFamily: 'monospace', color: Color(0xFF9e9e9e)),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text(
'CANCEL',
style: TextStyle(
fontFamily: 'monospace',
color: Color(0xFF616161),
),
),
),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text(
'CLEAR',
style: TextStyle(
fontFamily: 'monospace',
color: Color(0xFFff5252),
),
),
),
],
),
);
if (confirmed != true) return;
final result = await LocationBridge.clearLogs();
if (mounted) {
if (result >= 0) {
await _loadLogs();
}
}
}
String _formatTimestamp(int timestamp) {
final dt = DateTime.fromMillisecondsSinceEpoch(timestamp);
final ms = (dt.millisecond / 1000).toStringAsFixed(3).substring(2);
return '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}.$ms';
}
Color _getLogColor(String eventType) {
switch (eventType) {
case 'ERROR':
return const Color(0xFFff5252);
case 'WARNING':
return const Color(0xFFffb74d);
case 'INFO':
return const Color(0xFF00bcd4);
case 'LOCATION':
return const Color(0xFF69f0ae);
case 'SYNC':
return const Color(0xFF40c4ff);
case 'HEARTBEAT':
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);
}
}
IconData _getLogIcon(String eventType) {
switch (eventType) {
case 'ERROR':
return Icons.close;
case 'WARNING':
return Icons.warning;
case 'INFO':
return Icons.info;
case 'LOCATION':
return Icons.gps_fixed;
case 'SYNC':
return Icons.cloud_upload;
case 'HEARTBEAT':
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<Map<String, dynamic>> get _filteredLogs {
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();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0d0d0d),
appBar: AppBar(
backgroundColor: const Color(0xFF161616),
elevation: 0,
title: Row(
children: [
const Icon(Icons.terminal, color: Color(0xFF00bcd4), size: 18),
const SizedBox(width: 10),
const Text(
'EVENT LOG',
style: TextStyle(
fontFamily: 'monospace',
fontSize: 14,
fontWeight: FontWeight.w700,
letterSpacing: 2,
color: Color(0xFFe0e0e0),
),
),
],
),
leading: IconButton(
icon: const Icon(
Icons.arrow_back,
color: Color(0xFF9e9e9e),
size: 20,
),
onPressed: () => Navigator.pop(context),
),
actions: [
IconButton(
icon: const Icon(
Icons.delete_outline,
color: Color(0xFF616161),
size: 20,
),
onPressed: _clearLogs,
),
IconButton(
icon: const Icon(Icons.refresh, color: Color(0xFF616161), size: 20),
onPressed: _loadLogs,
),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1),
child: Container(height: 1, color: const Color(0xFF2a2a2a)),
),
),
body: Column(
children: [
// ── Filter bar ──────────────────────────────────────────────────────
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: const BoxDecoration(
color: Color(0xFF161616),
border: Border(
bottom: BorderSide(color: Color(0xFF2a2a2a), width: 1),
),
),
child: Row(
children: [
_buildFilterChip('NORMAL', 'NORMAL'),
const SizedBox(width: 8),
_buildFilterChip('LOCATION', 'LOCATION'),
const SizedBox(width: 8),
_buildFilterChip('SYNC', 'SYNC'),
const SizedBox(width: 8),
_buildFilterChip('ERR', 'ERROR'),
const SizedBox(width: 8),
_buildFilterChip('DEBUG', 'DEBUG'),
],
),
),
// ── Log list ─────────────────────────────────────────────────────────
Expanded(
child: _filteredLogs.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.inbox_outlined,
size: 48,
color: Color(0xFF2a2a2a),
),
const SizedBox(height: 16),
Text(
_filter == 'VERBOSE'
? 'No events recorded'
: 'No $_filter events',
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 13,
color: Color(0xFF424242),
),
),
const SizedBox(height: 8),
Text(
'Start tracking to capture events',
style: TextStyle(
fontFamily: 'monospace',
fontSize: 11,
color: const Color(
0xFF424242,
).withValues(alpha: 0.6),
),
),
],
),
)
: ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: _filteredLogs.length,
itemBuilder: (context, index) {
return _buildLogEntry(_filteredLogs[index], index);
},
),
),
],
),
);
}
Widget _buildFilterChip(String label, String eventType) {
final isSelected = _filter == eventType;
return GestureDetector(
onTap: () => setState(() => _filter = eventType),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: isSelected
? const Color(0xFF00bcd4).withValues(alpha: 0.15)
: Colors.transparent,
borderRadius: BorderRadius.circular(3),
border: Border.all(
color: isSelected
? const Color(0xFF00bcd4).withValues(alpha: 0.5)
: const Color(0xFF2a2a2a),
width: 1,
),
),
child: Text(
label,
style: TextStyle(
fontFamily: 'monospace',
fontSize: 10,
fontWeight: FontWeight.w700,
letterSpacing: 1,
color: isSelected
? const Color(0xFF00bcd4)
: const Color(0xFF424242),
),
),
),
);
}
Widget _buildLogEntry(Map<String, dynamic> log, int index) {
final time = _formatTimestamp(log['timestamp'] as int? ?? 0);
final type = log['eventType'] as String? ?? 'UNKNOWN';
final msg = log['message'] as String? ?? '';
final color = _getLogColor(type);
final icon = _getLogIcon(type);
return Padding(
padding: EdgeInsets.only(
left: 14,
right: 14,
top: index == 0 ? 8 : 10,
bottom: 8,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Line 1: timestamp | icon | type
Row(
children: [
Text(
time,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 10,
color: Color(0xFF546e7a),
),
),
const SizedBox(width: 8),
Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: color.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(3),
),
child: Icon(icon, size: 10, color: color),
),
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(2),
),
child: Text(
type,
style: TextStyle(
fontFamily: 'monospace',
fontSize: 9,
fontWeight: FontWeight.w700,
letterSpacing: 1,
color: color,
),
),
),
],
),
const SizedBox(height: 5),
// Line 2: message
SelectableText(
msg,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 11,
color: Color(0xFF9e9e9e),
),
),
],
),
);
}
}