import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:mqtt_client/mqtt_client.dart'; import '../services/mqtt_service.dart'; import '../services/trace_service.dart'; /// Displays the in-memory trace log for diagnosing message delivery problems. /// /// Shows entries in reverse chronological order (newest first). /// Accessible from the Settings screen via "Message Trace Log". /// Works in both debug and release builds. class TraceScreen extends StatefulWidget { /// Optional MQTT service reference so entries can be published to the server. final MqttService? mqttService; const TraceScreen({super.key, this.mqttService}); @override State createState() => _TraceScreenState(); } class _TraceScreenState extends State { bool _sending = false; List get _entries => TraceService.instance.entries.reversed.toList(); Future _sendToServer() async { final service = widget.mqttService; if (service == null || !service.isConnected) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Not connected to server')), ); } return; } setState(() => _sending = true); try { final entries = TraceService.instance.entries; final payload = jsonEncode({ 'type': 'trace_log', 'source': 'pailot', 'ts': DateTime.now().millisecondsSinceEpoch, 'count': entries.length, 'entries': entries .map((e) => { 'timestamp': e.timestamp.toIso8601String(), 'event': e.event, 'details': e.details, }) .toList(), }); // Build the MQTT payload final builder = MqttClientPayloadBuilder(); builder.addString(payload); // Publish on pailot/trace — daemon can subscribe/log this service.send({ 'type': 'command', 'command': 'trace_upload', 'args': { 'count': entries.length, 'payload': payload, }, }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Sent ${entries.length} trace entries to server'), ), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Send failed: $e')), ); } } finally { if (mounted) setState(() => _sending = false); } } void _clearLog() { TraceService.instance.clear(); setState(() {}); } Color _colorForEvent(String event) { if (event.contains('error') || event.contains('fail') || event.contains('drop')) { return Colors.red.shade300; } if (event.contains('dedup')) return Colors.orange.shade300; if (event.contains('displayed') || event.contains('published')) { return Colors.green.shade300; } if (event.contains('MQTT')) return Colors.blue.shade300; if (event.contains('voice')) return Colors.purple.shade300; return Colors.grey.shade400; } @override Widget build(BuildContext context) { final entries = _entries; return Scaffold( appBar: AppBar( title: const Text('Message Trace Log'), actions: [ IconButton( icon: const Icon(Icons.delete_outline), tooltip: 'Clear log', onPressed: _clearLog, ), IconButton( icon: _sending ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.upload_outlined), tooltip: 'Send to server', onPressed: _sending ? null : _sendToServer, ), ], ), body: entries.isEmpty ? const Center( child: Text( 'No trace entries yet.\nSend a message to generate traces.', textAlign: TextAlign.center, style: TextStyle(color: Colors.grey), ), ) : Column( children: [ Container( color: Colors.black87, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), child: Row( children: [ Text( '${entries.length} entries (newest first)', style: const TextStyle( fontSize: 11, color: Colors.grey, fontFamily: 'monospace', ), ), const Spacer(), const Text( 'Tap to copy', style: TextStyle(fontSize: 11, color: Colors.grey), ), ], ), ), Expanded( child: ListView.builder( itemCount: entries.length, itemBuilder: (context, index) { final entry = entries[index]; final ts = entry.timestamp .toIso8601String() .substring(11, 23); // HH:mm:ss.mmm return InkWell( onTap: () { // Copy full entry to clipboard on tap final text = '$ts | ${entry.event} | ${entry.details}'; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( text, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontFamily: 'monospace', fontSize: 11), ), duration: const Duration(seconds: 2), ), ); }, child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6, ), decoration: BoxDecoration( border: Border( bottom: BorderSide( color: Colors.grey.shade800, width: 0.5, ), ), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( ts, style: const TextStyle( fontFamily: 'monospace', fontSize: 10, color: Colors.grey, ), ), const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( entry.event, style: TextStyle( fontFamily: 'monospace', fontSize: 11, fontWeight: FontWeight.bold, color: _colorForEvent(entry.event), ), ), if (entry.details.isNotEmpty) Text( entry.details, style: const TextStyle( fontFamily: 'monospace', fontSize: 10, color: Colors.grey, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ), ), ], ), ), ); }, ), ), ], ), ); } }