import 'package:flutter/material.dart'; import '../control_plane_status_client.dart'; import 'runtime_panel_sections.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) => _executeCommand(operation); 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 { if (selector.isEmpty || command.isEmpty) return; await _executeCommand( 'agent.command', targetSelector: selector, parameters: {'command': command}, ); } 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 { if (selector.isEmpty) return; await _executeCommand( 'agent.status', targetSelector: selector, ); } Future _executeCommand( String operation, { String? targetSelector, Map? parameters, }) async { final edgeId = widget.selectedEdgeId; if (edgeId == null) return; setState(() { _isLoading = true; _lastCommandStatus = null; }); try { final resp = await widget.statusRepository.sendEdgeCommand( edgeId, operation, targetSelector: targetSelector, parameters: parameters, ); 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: [ _buildHeader(dropdownValue), const SizedBox(height: 24), if (_lastCommandStatus != null) ...[ _buildStatusBanner(), const SizedBox(height: 20), ], Expanded( child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildControls(), const SizedBox(height: 32), _buildDomainAgentsSection(selectedEdge), const SizedBox(height: 32), _buildOperationsHistorySection(), ], ), ), ), ], ), ); } Widget _buildHeader(String? dropdownValue) { return 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, ], ); } }, ); } Widget _buildStatusBanner() { final isSuccess = _lastCommandStatus!.startsWith('Success'); return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: isSuccess ? const Color(0xFF10B981).withValues(alpha: 0.1) : const Color(0xFFEF4444).withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), border: Border.all( color: isSuccess ? const Color(0xFF10B981).withValues(alpha: 0.3) : const Color(0xFFEF4444).withValues(alpha: 0.3), ), ), child: Row( children: [ Icon( isSuccess ? Icons.check_circle_outline : Icons.error_outline, color: isSuccess ? const Color(0xFF10B981) : const Color(0xFFEF4444), size: 20, ), const SizedBox(width: 12), Expanded( child: Text( _lastCommandStatus!, style: TextStyle( color: isSuccess ? 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; }); }, ), ], ), ); } Widget _buildControls() { return 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, ), ), ], ), ], ); } Widget _buildDomainAgentsSection(FleetEdgeView selectedEdge) { return RuntimePanelDomainAgentsSection(edge: selectedEdge); } Widget _buildOperationsHistorySection() { return RuntimePanelOperationsHistorySection( isLoading: _isLoading, error: _error, response: _operations, ); } }