import 'package:flutter/material.dart'; import '../control_plane_status_client.dart'; class RuntimePanelDomainAgentsSection extends StatelessWidget { final FleetEdgeView edge; const RuntimePanelDomainAgentsSection({required this.edge, super.key}); @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Domain Agents', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white), ), const SizedBox(height: 12), _DomainAgentsGrid(edge: edge), ], ); } } class RuntimePanelOperationsHistorySection extends StatelessWidget { final bool isLoading; final String? error; final EdgeOperationsResponseView? response; const RuntimePanelOperationsHistorySection({ required this.isLoading, required this.error, required this.response, super.key, }); @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Operations Execution History', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white), ), const SizedBox(height: 12), _OperationsHistoryList( isLoading: isLoading, error: error, response: response, ), ], ); } } class _DomainAgentsGrid extends StatelessWidget { final FleetEdgeView edge; const _DomainAgentsGrid({required this.edge}); @override Widget build(BuildContext context) { final agents = edge.domainAgents; if (agents.isEmpty) { return _DomainAgentsEmpty(); } 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) { return _DomainAgentCard(agent: agents[index]); }, ); }, ); } } class _DomainAgentsEmpty extends StatelessWidget { const _DomainAgentsEmpty(); @override Widget build(BuildContext context) { 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), ), ), ); } } class _DomainAgentCard extends StatelessWidget { final EdgeDomainAgentSummaryView agent; const _DomainAgentCard({required this.agent}); @override Widget build(BuildContext context) { 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(), ], ), ), ); } 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); } } } class _OperationsHistoryList extends StatelessWidget { final bool isLoading; final String? error; final EdgeOperationsResponseView? response; const _OperationsHistoryList({ required this.isLoading, required this.error, required this.response, }); @override Widget build(BuildContext context) { // not-loaded: fetch has not completed yet if (isLoading && response == null) { return const _OperationsHistoryLoading(); } // error from repository fetch if (error != null) { return _OperationsHistoryError(message: error!); } // loaded-empty: fetch completed with empty operations final operations = response?.operations ?? const []; if (operations.isEmpty) { return const _OperationsHistoryEmpty(); } return ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: operations.length, itemBuilder: (context, index) { return _OperationRecordCard(record: operations[index]); }, ); } } class _OperationsHistoryLoading extends StatelessWidget { const _OperationsHistoryLoading(); @override Widget build(BuildContext context) { return const Center( child: Padding( padding: EdgeInsets.all(24.0), child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(Color(0xFF6366F1)), ), ), ); } } class _OperationsHistoryError extends StatelessWidget { final String message; const _OperationsHistoryError({required this.message}); @override Widget build(BuildContext context) { return Center( child: Padding( padding: const EdgeInsets.all(24.0), child: Text('Error: $message', style: const TextStyle(color: Color(0xFFEF4444))), ), ); } } class _OperationsHistoryEmpty extends StatelessWidget { const _OperationsHistoryEmpty(); @override Widget build(BuildContext context) { 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), ), ), ); } } class _OperationRecordCard extends StatelessWidget { final EdgeCommandRecordView record; const _OperationRecordCard({required this.record}); @override Widget build(BuildContext context) { final opColor = _getOpStatusColor(record.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( record.status.toLowerCase() == 'success' ? Icons.check_circle : Icons.pending, color: opColor, ), title: Text(record.operation, style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.white)), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 4), Text( 'ID: ${record.commandId} | Selector: ${record.targetSelector.isNotEmpty ? record.targetSelector : "all"}', style: const TextStyle(color: Color(0xFF64748B), fontSize: 11), ), if (record.summary.isNotEmpty) ...[ const SizedBox(height: 4), Text(record.summary, style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 12)), ], if (record.error.isNotEmpty) ...[ const SizedBox(height: 4), Text('Error: ${record.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( record.status.toUpperCase(), style: TextStyle(color: opColor, fontSize: 10, fontWeight: FontWeight.bold), ), ), ), ); } 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); } } }