import 'package:flutter/material.dart'; import 'control_plane_status_client.dart'; class EdgesPanel extends StatelessWidget { final List edges; final bool isLoading; final String? error; final String? selectedEdgeId; final ValueChanged onSelectEdge; final VoidCallback onRefresh; const EdgesPanel({ super.key, required this.edges, required this.isLoading, required this.error, required this.selectedEdgeId, required this.onSelectEdge, required this.onRefresh, }); @override Widget build(BuildContext context) { if (isLoading && edges.isEmpty) { return const Center( child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(Color(0xFF6366F1)), ), ); } if (error != null && edges.isEmpty) { return Center( child: Container( padding: const EdgeInsets.all(24), margin: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFF1E293B), borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFFEF4444).withValues(alpha: 0.3)), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.error_outline, color: Color(0xFFEF4444), size: 48), const SizedBox(height: 16), const Text( 'Failed to Load Edges', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white), ), const SizedBox(height: 8), Text( error!, style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 13), textAlign: TextAlign.center, ), const SizedBox(height: 16), ElevatedButton.icon( onPressed: onRefresh, icon: const Icon(Icons.refresh), label: const Text('Retry'), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF6366F1), foregroundColor: Colors.white, ), ), ], ), ), ); } if (edges.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.dns_outlined, color: Color(0xFF6366F1), size: 64), const SizedBox(height: 16), const Text( 'No Edges Registered', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.white), ), const SizedBox(height: 8), const Text( 'Connect your edges to the Control Plane to see them here.', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 14), textAlign: TextAlign.center, ), const SizedBox(height: 20), OutlinedButton.icon( onPressed: onRefresh, icon: const Icon(Icons.refresh), label: const Text('Refresh'), style: OutlinedButton.styleFrom( foregroundColor: const Color(0xFF6366F1), side: const BorderSide(color: Color(0xFF6366F1)), ), ), ], ), ); } final selectedEdge = edges.firstWhere( (e) => e.edgeId == selectedEdgeId, orElse: () => edges.first, ); Widget buildEdgeList(bool isMobile) { return Container( decoration: BoxDecoration( border: isMobile ? const Border(bottom: BorderSide(color: Color(0xFF334155))) : const Border(right: BorderSide(color: Color(0xFF334155))), ), child: ListView.builder( itemCount: edges.length, itemBuilder: (context, index) { final edge = edges[index]; final isSelected = edge.edgeId == selectedEdge.edgeId; return InkWell( onTap: () => onSelectEdge(edge.edgeId), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( color: isSelected ? const Color(0xFF1E293B) : Colors.transparent, border: const Border( bottom: BorderSide(color: Color(0xFF334155), width: 0.5), ), ), child: Row( children: [ Container( width: 8, height: 8, decoration: BoxDecoration( shape: BoxShape.circle, color: edge.connected ? const Color(0xFF10B981) : const Color(0xFF64748B), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( edge.edgeName.isNotEmpty ? edge.edgeName : edge.edgeId, style: TextStyle( fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, color: isSelected ? const Color(0xFF6366F1) : Colors.white, fontSize: 15, ), ), const SizedBox(height: 4), Text( 'ID: ${edge.edgeId}', style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 12), ), ], ), ), const Icon(Icons.chevron_right, color: Color(0xFF64748B), size: 16), ], ), ), ); }, ), ); } Widget buildEdgeDetails(bool isMobile) { return Container( padding: EdgeInsets.all(isMobile ? 16 : 24), child: ListView( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( selectedEdge.edgeName.isNotEmpty ? selectedEdge.edgeName : selectedEdge.edgeId, style: TextStyle(fontSize: isMobile ? 18 : 22, fontWeight: FontWeight.bold, color: Colors.white), overflow: TextOverflow.ellipsis, ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: selectedEdge.connected ? const Color(0xFF10B981).withValues(alpha: 0.15) : const Color(0xFF64748B).withValues(alpha: 0.15), borderRadius: BorderRadius.circular(20), border: Border.all( color: selectedEdge.connected ? const Color(0xFF10B981) : const Color(0xFF64748B), width: 1.5, ), ), child: Text( selectedEdge.connected ? 'CONNECTED' : 'DISCONNECTED', style: TextStyle( color: selectedEdge.connected ? const Color(0xFF10B981) : const Color(0xFF94A3B8), fontSize: 11, fontWeight: FontWeight.bold, ), ), ), ], ), const SizedBox(height: 24), _buildDetailRow('Edge ID', selectedEdge.edgeId), _buildDetailRow('Version', selectedEdge.version), _buildDetailRow('Protocol', selectedEdge.protocol), _buildDetailRow('Last Seen', selectedEdge.lastSeen.toLocal().toString()), _buildDetailRow('Fleet Health', selectedEdge.health), _buildDetailRow('Node Count', selectedEdge.nodeCount.toString()), const SizedBox(height: 24), const Text( 'Capabilities', style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold, color: Colors.white), ), const SizedBox(height: 12), if (selectedEdge.capabilities.isEmpty) const Text('No capabilities reported', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 13)) else Wrap( spacing: 8, runSpacing: 8, children: selectedEdge.capabilities.map((cap) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: const Color(0xFF6366F1).withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), border: Border.all(color: const Color(0xFF6366F1).withValues(alpha: 0.3)), ), child: Text( cap.kind.isNotEmpty ? cap.kind : cap.toString(), style: const TextStyle(color: Color(0xFF818CF8), fontSize: 12, fontWeight: FontWeight.w500), ), ); }).toList(), ), ], ), ); } return LayoutBuilder( builder: (context, constraints) { final isMobile = constraints.maxWidth < 600; if (isMobile) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( flex: 2, child: buildEdgeList(true), ), Expanded( flex: 3, child: buildEdgeDetails(true), ), ], ); } else { return Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( flex: 2, child: buildEdgeList(false), ), Expanded( flex: 3, child: buildEdgeDetails(false), ), ], ); } }, ); } Widget _buildDetailRow(String label, String value) { return Padding( padding: const EdgeInsets.only(bottom: 14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 12)), const SizedBox(height: 4), Text( value.isNotEmpty ? value : '-', style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500), ), const SizedBox(height: 8), const Divider(color: Color(0xFF334155), height: 1), ], ), ); } } class NodesPanel extends StatelessWidget { final List edges; final String? selectedEdgeId; final EdgeStatusResponseView? selectedEdgeStatus; final bool isLoading; final String? error; final ValueChanged onSelectEdge; final VoidCallback onRefresh; const NodesPanel({ super.key, required this.edges, required this.selectedEdgeId, required this.selectedEdgeStatus, required this.isLoading, required this.error, required this.onSelectEdge, required this.onRefresh, }); @override Widget build(BuildContext context) { if (edges.isEmpty) { return const Center( child: Text( 'Please connect and register at least one Edge first.', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 14), ), ); } final currentEdgeId = selectedEdgeId ?? edges.first.edgeId; final hasCurrentEdge = edges.any((e) => e.edgeId == currentEdgeId); final dropdownValue = hasCurrentEdge ? currentEdgeId : (edges.isNotEmpty ? edges.first.edgeId : null); return Container( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Expanded( child: Text( 'Nodes View', style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white), overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 16), if (edges.isNotEmpty && dropdownValue != null) ...[ const Text('Active Edge: ', style: TextStyle(color: Color(0xFF94A3B8))), const SizedBox(width: 8), DropdownButton( value: dropdownValue, dropdownColor: const Color(0xFF1E293B), style: const TextStyle(color: Colors.white), underline: const SizedBox(), items: edges.map((e) { return DropdownMenuItem( value: e.edgeId, child: Text(e.edgeName.isNotEmpty ? e.edgeName : e.edgeId), ); }).toList(), onChanged: (val) { if (val != null) onSelectEdge(val); }, ), ], const SizedBox(width: 8), IconButton( icon: const Icon(Icons.refresh, color: Color(0xFF6366F1)), onPressed: onRefresh, ), ], ), const SizedBox(height: 20), Expanded(child: _buildNodeContent()), ], ), ); } Widget _buildNodeContent() { if (isLoading) { return const Center( child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(Color(0xFF6366F1)), ), ); } if (error != null) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error_outline, color: Color(0xFFEF4444), size: 48), const SizedBox(height: 16), const Text( 'Failed to Load Nodes Status', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white), ), const SizedBox(height: 8), Text(error!, style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 13)), ], ), ); } final nodes = selectedEdgeStatus?.nodes ?? []; if (nodes.isEmpty) { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.developer_board_off_outlined, color: Color(0xFF64748B), size: 56), const SizedBox(height: 16), Text( 'No Nodes Registered', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white), ), const SizedBox(height: 4), Text( 'No active nodes have registered on this edge.', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 13), ), ], ), ); } return ListView.builder( itemCount: nodes.length, itemBuilder: (context, index) { final node = nodes[index]; return Card( color: const Color(0xFF1E293B), margin: const EdgeInsets.only(bottom: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide( color: node.connected ? const Color(0xFF10B981).withValues(alpha: 0.3) : const Color(0xFF334155), ), ), child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( node.alias.isNotEmpty ? node.alias : node.nodeId, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white), overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Text( 'ID: ${node.nodeId}', style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 12), overflow: TextOverflow.ellipsis, ), ], ), ), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: node.connected ? const Color(0xFF10B981).withValues(alpha: 0.1) : const Color(0xFFEF4444).withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Text( node.connected ? 'CONNECTED' : 'OFFLINE', style: TextStyle( color: node.connected ? const Color(0xFF10B981) : const Color(0xFFEF4444), fontSize: 10, fontWeight: FontWeight.bold, ), ), ), ], ), if (node.label.isNotEmpty) ...[ const SizedBox(height: 12), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: const Color(0xFF334155), borderRadius: BorderRadius.circular(4), ), child: Text( 'Label: ${node.label}', style: const TextStyle(color: Colors.white, fontSize: 12), ), ), ], if (node.config != null) ...[ const SizedBox(height: 16), const Divider(color: Color(0xFF334155)), const SizedBox(height: 12), Row( children: [ const Icon(Icons.bolt, size: 16, color: Color(0xFF6366F1)), const SizedBox(width: 6), Text( 'Concurrency Limit: ${node.config!.concurrency}', style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 13), ), ], ), const SizedBox(height: 12), const Text( 'Adapters', style: TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold), ), const SizedBox(height: 8), if (node.config!.adapters.isEmpty) const Text('No active adapters configured', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 12)) else Wrap( spacing: 8, runSpacing: 8, children: node.config!.adapters.map((adapter) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: adapter.enabled ? const Color(0xFF10B981).withValues(alpha: 0.1) : const Color(0xFFEF4444).withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), border: Border.all( color: adapter.enabled ? const Color(0xFF10B981) : const Color(0xFFEF4444), width: 1, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( adapter.type, style: TextStyle( color: adapter.enabled ? const Color(0xFF10B981) : const Color(0xFFEF4444), fontSize: 12, fontWeight: FontWeight.w500, ), ), const SizedBox(width: 4), Icon( adapter.enabled ? Icons.check : Icons.close, size: 12, color: adapter.enabled ? const Color(0xFF10B981) : const Color(0xFFEF4444), ), ], ), ); }).toList(), ), ], ], ), ), ); }, ); } } class ExecutionLogsPanel extends StatelessWidget { final List edges; final String? selectedEdgeId; final List events; final bool isLoading; final String? error; final ValueChanged onSelectEdge; final VoidCallback onRefresh; const ExecutionLogsPanel({ super.key, required this.edges, required this.selectedEdgeId, required this.events, required this.isLoading, required this.error, required this.onSelectEdge, required this.onRefresh, }); @override Widget build(BuildContext context) { if (edges.isEmpty) { return const Center( child: Text( 'Please connect and register at least one Edge first.', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 14), ), ); } final currentEdgeId = selectedEdgeId ?? edges.first.edgeId; final hasCurrentEdge = edges.any((e) => e.edgeId == currentEdgeId); final dropdownValue = hasCurrentEdge ? currentEdgeId : (edges.isNotEmpty ? edges.first.edgeId : null); return Container( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Expanded( child: Text( 'Lifecycle Events & Logs', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.white), overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 16), if (edges.isNotEmpty && dropdownValue != null) ...[ const Text('Filter Edge: ', style: TextStyle(color: Color(0xFF94A3B8))), const SizedBox(width: 8), DropdownButton( value: dropdownValue, dropdownColor: const Color(0xFF1E293B), style: const TextStyle(color: Colors.white), underline: const SizedBox(), items: edges.map((e) { return DropdownMenuItem( value: e.edgeId, child: Text(e.edgeName.isNotEmpty ? e.edgeName : e.edgeId), ); }).toList(), onChanged: (val) { if (val != null) onSelectEdge(val); }, ), ], const SizedBox(width: 8), IconButton( icon: const Icon(Icons.refresh, color: Color(0xFF6366F1)), onPressed: onRefresh, ), ], ), const SizedBox(height: 20), Expanded(child: _buildLogsContent()), ], ), ); } Widget _buildLogsContent() { if (isLoading) { return const Center( child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(Color(0xFF6366F1)), ), ); } if (error != null) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error_outline, color: Color(0xFFEF4444), size: 48), const SizedBox(height: 16), const Text( 'Failed to Load Events', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white), ), const SizedBox(height: 8), Text(error!, style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 13)), ], ), ); } if (events.isEmpty) { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.history_toggle_off, color: Color(0xFF64748B), size: 56), const SizedBox(height: 16), Text( 'No Events Captured', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white), ), const SizedBox(height: 4), Text( 'No lifecycle events have been recorded for this edge yet.', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 13), ), ], ), ); } return ListView.builder( itemCount: events.length, itemBuilder: (context, index) { final event = events[index]; final eventColor = _getEventColor(event.type); final eventIcon = _getEventIcon(event.type); return Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFF1E293B), borderRadius: BorderRadius.circular(8), border: Border( left: BorderSide(color: eventColor, width: 4), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(eventIcon, size: 16, color: eventColor), const SizedBox(width: 8), Text( event.type.toUpperCase(), style: TextStyle(color: eventColor, fontSize: 12, fontWeight: FontWeight.bold), ), const Spacer(), Text( event.timestamp.toLocal().toString().substring(11, 19), style: const TextStyle(color: Color(0xFF64748B), fontSize: 12), ), ], ), const SizedBox(height: 8), Text( event.reason, style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500), ), const SizedBox(height: 8), Row( children: [ const Text('Node: ', style: TextStyle(color: Color(0xFF64748B), fontSize: 12)), Text( event.alias.isNotEmpty ? event.alias : (event.nodeId.isNotEmpty ? event.nodeId : 'Edge System'), style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 12), ), if (event.source.isNotEmpty) ...[ const SizedBox(width: 12), const Text('Source: ', style: TextStyle(color: Color(0xFF64748B), fontSize: 12)), Text( event.source, style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 12), ), ], ], ), ], ), ); }, ); } Color _getEventColor(String type) { switch (type.toLowerCase()) { case 'error': case 'failed': return const Color(0xFFEF4444); case 'warning': case 'warn': return const Color(0xFFF59E0B); case 'success': case 'connected': case 'online': return const Color(0xFF10B981); case 'info': default: return const Color(0xFF6366F1); } } IconData _getEventIcon(String type) { switch (type.toLowerCase()) { case 'error': case 'failed': return Icons.error_outline; case 'warning': case 'warn': return Icons.warning_amber_outlined; case 'success': case 'connected': case 'online': return Icons.check_circle_outline; case 'info': default: return Icons.info_outline; } } } class RuntimePanel extends StatefulWidget { final List edges; final String? selectedEdgeId; final ValueChanged onSelectEdge; final ControlPlaneStatusRepository statusRepository; const RuntimePanel({ super.key, required this.edges, required this.selectedEdgeId, required this.onSelectEdge, required this.statusRepository, }); @override State createState() => _RuntimePanelState(); } class _RuntimePanelState extends State { bool _isLoading = false; String? _error; EdgeOperationsResponseView? _operations; String? _lastCommandStatus; @override void initState() { super.initState(); _fetchOperations(); } @override void didUpdateWidget(RuntimePanel oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.selectedEdgeId != widget.selectedEdgeId) { _fetchOperations(); } } Future _fetchOperations() async { final edgeId = widget.selectedEdgeId; if (edgeId == null) return; setState(() { _isLoading = true; _error = null; }); try { final ops = await widget.statusRepository.fetchEdgeOperations(edgeId); if (mounted) { setState(() { _operations = ops; _isLoading = false; }); } } catch (e) { if (mounted) { setState(() { _error = e.toString(); _isLoading = false; }); } } } Future _sendCommand(String operation) async { final edgeId = widget.selectedEdgeId; if (edgeId == null) return; setState(() { _isLoading = true; _lastCommandStatus = null; }); try { final resp = await widget.statusRepository.sendEdgeCommand(edgeId, operation); if (mounted) { final isError = resp.status.toLowerCase() == 'error' || resp.status.toLowerCase() == 'failed' || resp.status.toLowerCase() == 'unsupported' || resp.error.isNotEmpty; setState(() { if (isError) { _lastCommandStatus = 'Failed: ${resp.status} - ${resp.error.isNotEmpty ? resp.error : resp.summary}'; } else { _lastCommandStatus = 'Success: Command accepted (${resp.status}) - ID: ${resp.commandId}'; } _isLoading = false; }); _fetchOperations(); } } catch (e) { if (mounted) { setState(() { _lastCommandStatus = 'Failed: ${e.toString()}'; _isLoading = false; }); } } } void _showAgentCommandDialog() { final selectorController = TextEditingController(); final commandController = TextEditingController(); showDialog( context: context, builder: (context) { return AlertDialog( backgroundColor: const Color(0xFF1E293B), title: const Text('Send Agent Command', style: TextStyle(color: Colors.white)), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: selectorController, style: const TextStyle(color: Colors.white), decoration: const InputDecoration( labelText: 'Target Selector (e.g. node-1)', labelStyle: TextStyle(color: Color(0xFF94A3B8)), ), ), const SizedBox(height: 12), TextField( controller: commandController, style: const TextStyle(color: Colors.white), decoration: const InputDecoration( labelText: 'Command Name (parameters.command)', labelStyle: TextStyle(color: Color(0xFF94A3B8)), ), ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Cancel', style: TextStyle(color: Color(0xFF94A3B8))), ), ElevatedButton( onPressed: () { final selector = selectorController.text.trim(); final cmd = commandController.text.trim(); if (selector.isNotEmpty && cmd.isNotEmpty) { Navigator.pop(context); _sendAgentCommand(selector, cmd); } }, child: const Text('Send'), ), ], ); }, ); } Future _sendAgentCommand(String selector, String command) async { final edgeId = widget.selectedEdgeId; if (edgeId == null) return; if (selector.isEmpty || command.isEmpty) return; setState(() { _isLoading = true; _lastCommandStatus = null; }); try { final resp = await widget.statusRepository.sendEdgeCommand( edgeId, 'agent.command', targetSelector: selector, parameters: {'command': command}, ); if (mounted) { final isError = resp.status.toLowerCase() == 'error' || resp.status.toLowerCase() == 'failed' || resp.status.toLowerCase() == 'unsupported' || resp.error.isNotEmpty; setState(() { if (isError) { _lastCommandStatus = 'Failed: ${resp.status} - ${resp.error.isNotEmpty ? resp.error : resp.summary}'; } else { _lastCommandStatus = 'Success: Command accepted (${resp.status}) - ID: ${resp.commandId}'; } _isLoading = false; }); _fetchOperations(); } } catch (e) { if (mounted) { setState(() { _lastCommandStatus = 'Failed: ${e.toString()}'; _isLoading = false; }); } } } void _showAgentStatusDialog() { final selectorController = TextEditingController(); showDialog( context: context, builder: (context) { return AlertDialog( backgroundColor: const Color(0xFF1E293B), title: const Text('Get Agent Status', style: TextStyle(color: Colors.white)), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: selectorController, style: const TextStyle(color: Colors.white), decoration: const InputDecoration( labelText: 'Target Selector (e.g. node-1)', labelStyle: TextStyle(color: Color(0xFF94A3B8)), ), ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Cancel', style: TextStyle(color: Color(0xFF94A3B8))), ), ElevatedButton( onPressed: () { final selector = selectorController.text.trim(); if (selector.isNotEmpty) { Navigator.pop(context); _sendAgentStatus(selector); } }, child: const Text('Send'), ), ], ); }, ); } Future _sendAgentStatus(String selector) async { final edgeId = widget.selectedEdgeId; if (edgeId == null) return; if (selector.isEmpty) return; setState(() { _isLoading = true; _lastCommandStatus = null; }); try { final resp = await widget.statusRepository.sendEdgeCommand( edgeId, 'agent.status', targetSelector: selector, ); if (mounted) { final isError = resp.status.toLowerCase() == 'error' || resp.status.toLowerCase() == 'failed' || resp.status.toLowerCase() == 'unsupported' || resp.error.isNotEmpty; setState(() { if (isError) { _lastCommandStatus = 'Failed: ${resp.status} - ${resp.error.isNotEmpty ? resp.error : resp.summary}'; } else { _lastCommandStatus = 'Success: Command accepted (${resp.status}) - ID: ${resp.commandId}'; } _isLoading = false; }); _fetchOperations(); } } catch (e) { if (mounted) { setState(() { _lastCommandStatus = 'Failed: ${e.toString()}'; _isLoading = false; }); } } } @override Widget build(BuildContext context) { if (widget.edges.isEmpty) { return const Center( child: Text( 'Please connect and register at least one Edge first.', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 14), ), ); } final currentEdgeId = widget.selectedEdgeId ?? widget.edges.first.edgeId; final hasCurrentEdge = widget.edges.any((e) => e.edgeId == currentEdgeId); final dropdownValue = hasCurrentEdge ? currentEdgeId : (widget.edges.isNotEmpty ? widget.edges.first.edgeId : null); final selectedEdge = hasCurrentEdge ? widget.edges.firstWhere((e) => e.edgeId == currentEdgeId) : widget.edges.first; return Container( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ LayoutBuilder( builder: (context, headerConstraints) { final isNarrow = headerConstraints.maxWidth < 500; final headerTitle = const Text( 'Operations & Domain Agents', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.white), overflow: TextOverflow.ellipsis, ); final headerControls = Wrap( crossAxisAlignment: WrapCrossAlignment.center, spacing: 8, runSpacing: 8, children: [ if (widget.edges.isNotEmpty && dropdownValue != null) ...[ const Text('Active Edge: ', style: TextStyle(color: Color(0xFF94A3B8))), DropdownButton( value: dropdownValue, dropdownColor: const Color(0xFF1E293B), style: const TextStyle(color: Colors.white), underline: const SizedBox(), items: widget.edges.map((e) { return DropdownMenuItem( value: e.edgeId, child: Text(e.edgeName.isNotEmpty ? e.edgeName : e.edgeId), ); }).toList(), onChanged: (val) { if (val != null) widget.onSelectEdge(val); }, ), ], IconButton( icon: const Icon(Icons.refresh, color: Color(0xFF6366F1)), onPressed: _fetchOperations, ), ], ); if (isNarrow) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ headerTitle, const SizedBox(height: 12), headerControls, ], ); } else { return Row( children: [ Expanded(child: headerTitle), const SizedBox(width: 16), headerControls, ], ); } }, ), const SizedBox(height: 24), if (_lastCommandStatus != null) ...[ Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: _lastCommandStatus!.startsWith('Success') ? const Color(0xFF10B981).withValues(alpha: 0.1) : const Color(0xFFEF4444).withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), border: Border.all( color: _lastCommandStatus!.startsWith('Success') ? const Color(0xFF10B981).withValues(alpha: 0.3) : const Color(0xFFEF4444).withValues(alpha: 0.3), ), ), child: Row( children: [ Icon( _lastCommandStatus!.startsWith('Success') ? Icons.check_circle_outline : Icons.error_outline, color: _lastCommandStatus!.startsWith('Success') ? const Color(0xFF10B981) : const Color(0xFFEF4444), size: 20, ), const SizedBox(width: 12), Expanded( child: Text( _lastCommandStatus!, style: TextStyle( color: _lastCommandStatus!.startsWith('Success') ? const Color(0xFFD1FAE5) : const Color(0xFFFEE2E2), fontSize: 13, fontWeight: FontWeight.w500, ), ), ), IconButton( icon: const Icon(Icons.close, size: 16, color: Color(0xFF94A3B8)), onPressed: () { setState(() { _lastCommandStatus = null; }); }, ), ], ), ), const SizedBox(height: 20), ], Expanded( child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'System Gated Operations', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white), ), const SizedBox(height: 12), Wrap( spacing: 12, runSpacing: 12, children: [ ElevatedButton.icon( onPressed: _isLoading ? null : () => _sendCommand('health.check'), icon: const Icon(Icons.health_and_safety_outlined, size: 16), label: const Text('Health Check'), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF10B981), foregroundColor: Colors.white, ), ), ElevatedButton.icon( onPressed: _isLoading ? null : _showAgentStatusDialog, icon: const Icon(Icons.info_outline, size: 16), label: const Text('Agent Status'), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF3B82F6), foregroundColor: Colors.white, ), ), ElevatedButton.icon( onPressed: _isLoading ? null : _showAgentCommandDialog, icon: const Icon(Icons.terminal_outlined, size: 16), label: const Text('Agent Command'), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF6366F1), foregroundColor: Colors.white, ), ), ], ), const SizedBox(height: 32), const Text( 'Domain Agents', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white), ), const SizedBox(height: 12), _buildDomainAgentsGrid(selectedEdge), const SizedBox(height: 32), const Text( 'Operations Execution History', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white), ), const SizedBox(height: 12), _buildOperationsHistory(), ], ), ), ), ], ), ); } Widget _buildDomainAgentsGrid(FleetEdgeView edge) { final agents = edge.domainAgents; if (agents.isEmpty) { return Container( padding: const EdgeInsets.all(24), width: double.infinity, decoration: BoxDecoration( color: const Color(0xFF1E293B), borderRadius: BorderRadius.circular(12), ), child: const Center( child: Text( 'No Domain Agents reported for this Edge.', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 13), ), ), ); } return LayoutBuilder( builder: (context, constraints) { final crossAxisCount = constraints.maxWidth > 600 ? 2 : 1; return GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: agents.length, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: crossAxisCount, crossAxisSpacing: 16, mainAxisSpacing: 16, mainAxisExtent: 150, ), itemBuilder: (context, index) { final agent = agents[index]; final stateColor = _getLifecycleStateColor(agent.lifecycleState); return Card( color: const Color(0xFF1E293B), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide(color: stateColor.withValues(alpha: 0.3)), ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( agent.agentKind.toUpperCase(), style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold, color: Colors.white), overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: stateColor.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(20), ), child: Text( agent.lifecycleState.toUpperCase(), style: TextStyle(color: stateColor, fontSize: 10, fontWeight: FontWeight.bold), ), ), ], ), const SizedBox(height: 8), Text( agent.summary.isNotEmpty ? agent.summary : 'Available: ${agent.available}', style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 12), maxLines: 2, overflow: TextOverflow.ellipsis, ), if (agent.activeCommandId.isNotEmpty) ...[ const SizedBox(height: 6), Text( 'Active Command: ${agent.activeCommandId}', style: const TextStyle(color: Color(0xFF818CF8), fontSize: 11, fontFamily: 'monospace'), overflow: TextOverflow.ellipsis, ), ], const Spacer(), ], ), ), ); }, ); }, ); } Widget _buildOperationsHistory() { if (_isLoading && _operations == null) { return const Center( child: Padding( padding: EdgeInsets.all(24.0), child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(Color(0xFF6366F1)), ), ), ); } if (_error != null) { return Center( child: Padding( padding: const EdgeInsets.all(24.0), child: Text('Error: $_error', style: const TextStyle(color: Color(0xFFEF4444))), ), ); } final ops = _operations?.operations ?? []; if (ops.isEmpty) { return Container( padding: const EdgeInsets.all(24), width: double.infinity, decoration: BoxDecoration( color: const Color(0xFF1E293B), borderRadius: BorderRadius.circular(12), ), child: const Center( child: Text( 'No operation executions recorded.', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 13), ), ), ); } return ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: ops.length, itemBuilder: (context, index) { final op = ops[index]; final opColor = _getOpStatusColor(op.status); return Card( color: const Color(0xFF1E293B), margin: const EdgeInsets.only(bottom: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), side: BorderSide(color: const Color(0xFF334155)), ), child: ListTile( leading: Icon( op.status.toLowerCase() == 'success' ? Icons.check_circle : Icons.pending, color: opColor, ), title: Text(op.operation, style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.white)), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 4), Text( 'ID: ${op.commandId} | Selector: ${op.targetSelector.isNotEmpty ? op.targetSelector : "all"}', style: const TextStyle(color: Color(0xFF64748B), fontSize: 11), ), if (op.summary.isNotEmpty) ...[ const SizedBox(height: 4), Text(op.summary, style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 12)), ], if (op.error.isNotEmpty) ...[ const SizedBox(height: 4), Text('Error: ${op.error}', style: const TextStyle(color: Color(0xFFEF4444), fontSize: 12)), ], ], ), trailing: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: opColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(4), ), child: Text( op.status.toUpperCase(), style: TextStyle(color: opColor, fontSize: 10, fontWeight: FontWeight.bold), ), ), ), ); }, ); } Color _getLifecycleStateColor(String state) { switch (state.toLowerCase()) { case 'ready': case 'success': return const Color(0xFF10B981); case 'busy': case 'running': return const Color(0xFFF59E0B); case 'error': case 'failed': return const Color(0xFFEF4444); case 'unknown': default: return const Color(0xFF94A3B8); } } Color _getOpStatusColor(String status) { switch (status.toLowerCase()) { case 'success': return const Color(0xFF10B981); case 'accepted': case 'pending': return const Color(0xFFF59E0B); case 'error': case 'failed': return const Color(0xFFEF4444); default: return const Color(0xFF94A3B8); } } }