import 'package:flutter/material.dart'; import '../control_plane_status_client.dart'; 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; _operations = null; }); try { final ops = await widget.statusRepository.fetchEdgeOperations(edgeId); if (mounted && widget.selectedEdgeId == edgeId) { setState(() { _operations = ops; _isLoading = false; }); } } catch (e) { if (mounted && widget.selectedEdgeId == edgeId) { 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); } } }