import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../shared/design_system/unionflow_design_system.dart'; import '../../../../core/di/injection_container.dart'; import '../bloc/logs_monitoring_bloc.dart'; import '../../data/models/system_log_model.dart'; import '../../data/models/system_alert_model.dart'; /// Page Logs & Monitoring - UnionFlow Mobile /// /// Page complète de consultation des logs système avec monitoring en temps réel, /// alertes, métriques système et gestion avancée des journaux. class LogsPage extends StatelessWidget { const LogsPage({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (_) => sl()..add(LoadMetrics()), child: const _LogsView(), ); } } class _LogsView extends StatefulWidget { const _LogsView(); @override State<_LogsView> createState() => _LogsViewState(); } class _LogsViewState extends State<_LogsView> with TickerProviderStateMixin { late TabController _tabController; Timer? _refreshTimer; final TextEditingController _searchController = TextEditingController(); // États de filtrage String _selectedLevel = 'Tous'; String _selectedTimeRange = 'Dernières 24h'; String _selectedSource = 'Tous'; String _searchQuery = ''; bool _autoRefresh = true; bool _isLiveMode = false; // Données réelles du BLoC List _currentLogs = []; List _currentAlerts = []; bool _isLoadingLogs = false; // Données de configuration final List _levels = ['Tous', 'CRITICAL', 'ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE']; final List _timeRanges = ['Temps réel', 'Dernière heure', 'Dernières 24h', 'Dernière semaine', 'Dernier mois']; final List _sources = ['Tous', 'API', 'Auth', 'Database', 'Cache', 'Security', 'Performance', 'System']; // Métriques système final Map _systemMetrics = { 'cpu': 23.5, 'memory': 67.2, 'disk': 45.8, 'network': 12.3, 'activeConnections': 1247, 'errorRate': 0.02, 'responseTime': 127, 'uptime': '15j 7h 23m', }; @override void initState() { super.initState(); _tabController = TabController(length: 4, vsync: this); _startAutoRefresh(); // Charger les données initiales après le premier frame WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { context.read() ..add(SearchLogs()) ..add(LoadAlerts()); } }); } @override void dispose() { _tabController.dispose(); _searchController.dispose(); _refreshTimer?.cancel(); super.dispose(); } void _startAutoRefresh() { _refreshTimer?.cancel(); if (_autoRefresh) { _refreshTimer = Timer.periodic(const Duration(seconds: 10), (timer) { if (!mounted) return; context.read().add(LoadMetrics()); context.read().add(LoadAlerts()); if (_isLiveMode) { _dispatchSearchLogs(); } }); } } @override Widget build(BuildContext context) { return BlocConsumer( listener: (context, state) { if (state is MetricsLoaded) { // setState doit être appelé dans le listener, jamais dans builder _updateSystemMetricsFromState(state.metrics); } else if (state is LogsLoaded) { setState(() { _currentLogs = state.logs; _isLoadingLogs = false; }); } else if (state is AlertsLoaded) { setState(() { _currentAlerts = state.alerts; }); } else if (state is LogsMonitoringSuccess) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(state.message), backgroundColor: ColorTokens.success, behavior: SnackBarBehavior.floating, ), ); } else if (state is LogsMonitoringError) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(state.error), backgroundColor: ColorTokens.error, behavior: SnackBarBehavior.floating, ), ); } }, builder: (context, state) { return Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: UFAppBar( title: 'Logs & Monitoring', moduleGradient: ModuleColors.logsGradient, automaticallyImplyLeading: true, actions: [ IconButton( onPressed: () => _toggleLiveMode(), icon: Icon( _isLiveMode ? Icons.stop_circle : Icons.play_circle, color: _isLiveMode ? AppColors.successUI : AppColors.onGradient, ), tooltip: _isLiveMode ? 'Arrêter le mode temps réel' : 'Mode temps réel', ), IconButton( onPressed: () => _showExportDialog(), icon: const Icon(Icons.download, color: AppColors.onGradient), tooltip: 'Exporter les données', ), ], ), body: SafeArea( top: false, child: Column( children: [ _buildSystemIndicators(), _buildTabBar(), Expanded( child: TabBarView( controller: _tabController, children: [ _buildDashboardTab(), _buildLogsTab(), _buildAlertsTab(), _buildMetricsTab(), ], ), ), ], ), ), ); }, ); } /// Indicateurs système KPI (remplace le header custom) Widget _buildSystemIndicators() { final isDark = Theme.of(context).brightness == Brightness.dark; return Container( margin: const EdgeInsets.fromLTRB(SpacingTokens.lg, SpacingTokens.md, SpacingTokens.lg, 0), padding: const EdgeInsets.symmetric(horizontal: SpacingTokens.md, vertical: SpacingTokens.md), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(SpacingTokens.radiusLg), border: Border.all(color: isDark ? AppColors.borderDark : AppColors.border), boxShadow: const [BoxShadow(color: AppColors.shadow, blurRadius: 8, offset: Offset(0, 2))], ), child: Row( children: [ Expanded(child: _buildKpiIndicator('CPU', '${(_systemMetrics['cpu'] as num).toDouble().toStringAsFixed(0)}%', Icons.memory, _getCpuColor())), Container(width: 1, height: 36, color: isDark ? AppColors.borderDark : AppColors.border), Expanded(child: _buildKpiIndicator('RAM', '${(_systemMetrics['memory'] as num).toDouble().toStringAsFixed(0)}%', Icons.storage, _getMemoryColor())), Container(width: 1, height: 36, color: isDark ? AppColors.borderDark : AppColors.border), Expanded(child: _buildKpiIndicator('Disque', '${(_systemMetrics['disk'] as num).toDouble().toStringAsFixed(0)}%', Icons.sd_storage, AppColors.warning)), Container(width: 1, height: 36, color: isDark ? AppColors.borderDark : AppColors.border), Expanded(child: _buildKpiIndicator('Uptime', _systemMetrics['uptime'] as String, Icons.schedule, AppColors.success)), ], ), ); } Widget _buildKpiIndicator(String label, String value, IconData icon, Color color) { final isDark = Theme.of(context).brightness == Brightness.dark; return Column( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.all(SpacingTokens.sm), decoration: BoxDecoration(color: color.withOpacity(0.1), shape: BoxShape.circle), child: Icon(icon, color: color, size: 16), ), const SizedBox(height: SpacingTokens.xs), Text( value, style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: isDark ? AppColors.textPrimaryDark : AppColors.textPrimary, ), ), Text( label, style: TextStyle( fontSize: 10, color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondary, ), ), ], ); } Color _getCpuColor() { final cpu = (_systemMetrics['cpu'] as num).toDouble(); if (cpu > 80) return ColorTokens.error; if (cpu > 60) return ColorTokens.warning; return ColorTokens.success; } Color _getMemoryColor() { final memory = (_systemMetrics['memory'] as num).toDouble(); if (memory > 85) return ColorTokens.error; if (memory > 70) return ColorTokens.warning; return ColorTokens.success; } /// Barre d'onglets réorganisée Widget _buildTabBar() { return Container( margin: const EdgeInsets.fromLTRB(SpacingTokens.lg, SpacingTokens.md, SpacingTokens.lg, 0), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(SpacingTokens.radiusXl), boxShadow: const [ BoxShadow( color: AppColors.shadow, blurRadius: 10, offset: Offset(0, 2), ), ], ), child: TabBar( controller: _tabController, labelColor: ColorTokens.primary, unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant, indicatorColor: ColorTokens.primary, indicatorWeight: 3, labelStyle: TypographyTokens.labelSmall.copyWith(fontWeight: FontWeight.w600), isScrollable: true, tabs: const [ Tab(icon: Icon(Icons.dashboard, size: 16), text: 'Dashboard'), Tab(icon: Icon(Icons.list_alt, size: 16), text: 'Logs'), Tab(icon: Icon(Icons.notification_important, size: 16), text: 'Alertes'), Tab(icon: Icon(Icons.analytics, size: 16), text: 'Métriques'), ], ), ); } // ==================== MÉTHODES D'ACTION ==================== void _dispatchSearchLogs() { setState(() => _isLoadingLogs = true); context.read().add(SearchLogs( level: _selectedLevel == 'Tous' ? null : _selectedLevel, source: _selectedSource == 'Tous' ? null : _selectedSource, searchQuery: _searchQuery.isEmpty ? null : _searchQuery, timeRange: _selectedTimeRange == 'Temps réel' ? null : _selectedTimeRange, )); } void _toggleLiveMode() { setState(() { _isLiveMode = !_isLiveMode; if (_isLiveMode) { _selectedTimeRange = 'Temps réel'; _startAutoRefresh(); } else { _refreshTimer?.cancel(); } }); _showSuccessSnackBar(_isLiveMode ? 'Mode temps réel activé' : 'Mode temps réel désactivé'); } void _showExportDialog() { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Exporter les données'), content: Column( mainAxisSize: MainAxisSize.min, children: [ const Text('Sélectionnez les données à exporter :'), const SizedBox(height: SpacingTokens.xl), CheckboxListTile( title: const Text('Logs système'), value: true, onChanged: (value) {}, ), CheckboxListTile( title: const Text('Métriques'), value: true, onChanged: (value) {}, ), CheckboxListTile( title: const Text('Alertes'), value: false, onChanged: (value) {}, ), ], ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Annuler'), ), ElevatedButton( onPressed: () { Navigator.of(context).pop(); _exportLogs(); }, style: ElevatedButton.styleFrom( backgroundColor: ColorTokens.primary, foregroundColor: AppColors.onPrimary, ), child: const Text('Exporter'), ), ], ), ); } void _exportLogs() { _showSuccessSnackBar('Export des données lancé - Vous recevrez un email'); } // ==================== ONGLETS PRINCIPAUX ==================== /// Onglet 1 — Dashboard (vue d'ensemble non-redondante) Widget _buildDashboardTab() { return RefreshIndicator( color: ModuleColors.logs, onRefresh: () async { context.read() ..add(LoadMetrics()) ..add(LoadAlerts()); }, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(SpacingTokens.lg), child: Column( children: [ const SizedBox(height: SpacingTokens.xl), _buildSystemStatus(), const SizedBox(height: SpacingTokens.xl), _buildLogSummary24h(), const SizedBox(height: SpacingTokens.xl), _buildAlertsSummaryCard(), const SizedBox(height: 80), ], ), ), // SingleChildScrollView ); // RefreshIndicator } /// Statut système Widget _buildSystemStatus() { return UFInfoCard( title: 'État du système', icon: Icons.health_and_safety, trailing: Container( padding: const EdgeInsets.symmetric(horizontal: SpacingTokens.lg, vertical: SpacingTokens.sm), decoration: BoxDecoration( color: ColorTokens.success, borderRadius: BorderRadius.circular(SpacingTokens.radiusCircular), ), child: Text( 'OPÉRATIONNEL', style: TypographyTokens.labelSmall.copyWith( color: AppColors.onPrimary, fontWeight: FontWeight.w600, ), ), ), child: Column( children: [ Row( children: [ Expanded(child: _buildServiceStatus('API Server', true)), const SizedBox(width: SpacingTokens.lg), Expanded(child: _buildServiceStatus('Database', true)), ], ), const SizedBox(height: SpacingTokens.lg), Row( children: [ Expanded(child: _buildServiceStatus('Keycloak', true)), const SizedBox(width: SpacingTokens.lg), Expanded(child: _buildServiceStatus('CDN', false)), ], ), ], ), ); } Widget _buildServiceStatus(String service, bool isOnline) { return Container( padding: const EdgeInsets.all(SpacingTokens.lg), decoration: BoxDecoration( color: isOnline ? ColorTokens.success.withOpacity(0.05) : ColorTokens.error.withOpacity(0.05), borderRadius: BorderRadius.circular(SpacingTokens.radiusLg), border: Border.all( color: isOnline ? ColorTokens.success.withOpacity(0.2) : ColorTokens.error.withOpacity(0.2), ), ), child: Row( children: [ Icon( Icons.circle, color: isOnline ? ColorTokens.success : ColorTokens.error, size: 12, ), const SizedBox(width: SpacingTokens.md), Expanded( child: Text( service, style: TypographyTokens.bodySmall.copyWith( fontWeight: FontWeight.w600, color: Theme.of(context).colorScheme.onSurface, ), ), ), Text( isOnline ? 'OK' : 'DOWN', style: TypographyTokens.labelSmall.copyWith( color: isOnline ? ColorTokens.success : ColorTokens.error, fontWeight: FontWeight.w600, ), ), ], ), ); } /// Activité — Dernières 24h (remplace _buildQuickStats) Widget _buildLogSummary24h() { return UFInfoCard( title: 'Activité — Dernières 24h', icon: Icons.bar_chart, child: Column( children: [ Row( children: [ Expanded(child: _buildStatItem('Logs totaux', '${_systemMetrics['totalLogs'] ?? 15247}', Icons.list_alt, AppColors.info)), const SizedBox(width: SpacingTokens.lg), Expanded(child: _buildStatItem('Erreurs', '${_systemMetrics['errors'] ?? 23}', Icons.error_outline, AppColors.error)), ], ), const SizedBox(height: SpacingTokens.lg), Row( children: [ Expanded(child: _buildStatItem('Warnings', '${_systemMetrics['warnings'] ?? 156}', Icons.warning_amber, AppColors.warning)), const SizedBox(width: SpacingTokens.lg), Expanded(child: _buildStatItem('Connexions', '${_systemMetrics['activeConnections'] ?? 1247}', Icons.people_outline, AppColors.success)), ], ), const SizedBox(height: SpacingTokens.lg), Row( children: [ Expanded(child: _buildStatItem('Taux erreur', '${(((_systemMetrics['errorRate'] as num?)?.toDouble() ?? 0.02) * 100).toStringAsFixed(1)}%', Icons.percent, AppColors.warning)), const SizedBox(width: SpacingTokens.lg), Expanded(child: _buildStatItem('Tps réponse', '${_systemMetrics['responseTime'] ?? 127}ms', Icons.timer_outlined, AppColors.primaryDark)), ], ), ], ), ); } Widget _buildStatItem(String label, String value, IconData icon, Color color) { return Container( padding: const EdgeInsets.all(SpacingTokens.lg), decoration: BoxDecoration( color: color.withOpacity(0.05), borderRadius: BorderRadius.circular(SpacingTokens.radiusLg), border: Border.all(color: color.withOpacity(0.1)), ), child: Column( children: [ Icon(icon, color: color, size: 20), const SizedBox(height: SpacingTokens.md), Text( value, style: TypographyTokens.titleMedium.copyWith( fontWeight: FontWeight.bold, color: color, ), ), Text( label, style: TypographyTokens.bodySmall.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), textAlign: TextAlign.center, ), ], ), ); } /// Résumé alertes dashboard (remplace _buildRecentAlerts) Widget _buildAlertsSummaryCard() { final unacknowledged = _currentAlerts.where((a) => a.acknowledged == false).toList(); return UFInfoCard( title: 'Alertes', icon: Icons.notifications_outlined, trailing: TextButton.icon( onPressed: () => _tabController.animateTo(2), icon: const Icon(Icons.arrow_forward, size: 14), label: const Text('Gérer'), ), child: unacknowledged.isEmpty ? Row( children: [ Container( padding: const EdgeInsets.all(SpacingTokens.md), decoration: BoxDecoration( color: AppColors.successContainer, shape: BoxShape.circle, ), child: Icon(Icons.check_circle, color: AppColors.success, size: 20), ), const SizedBox(width: SpacingTokens.lg), Text( 'Aucune alerte active', style: TextStyle( color: AppColors.success, fontWeight: FontWeight.w600, ), ), ], ) : Column( children: [ Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: SpacingTokens.lg, vertical: SpacingTokens.sm), decoration: BoxDecoration( color: AppColors.errorContainer, borderRadius: BorderRadius.circular(SpacingTokens.radiusCircular), ), child: Text( '${unacknowledged.length} non acquittée${unacknowledged.length > 1 ? 's' : ''}', style: TextStyle(color: AppColors.error, fontWeight: FontWeight.bold, fontSize: 12), ), ), ], ), const SizedBox(height: SpacingTokens.lg), ...unacknowledged.take(2).map((alert) => _buildAlertItem(alert)), ], ), ); } Widget _buildAlertItem(SystemAlertModel alert) { final color = _getAlertColor(alert.level ?? 'INFO'); return Container( margin: const EdgeInsets.only(bottom: SpacingTokens.lg), padding: const EdgeInsets.all(SpacingTokens.lg), decoration: BoxDecoration( color: color.withOpacity(0.05), borderRadius: BorderRadius.circular(SpacingTokens.radiusLg), border: Border.all(color: color.withOpacity(0.2)), ), child: Row( children: [ Icon(_getAlertIcon(alert.level ?? 'INFO'), color: color, size: 20), const SizedBox(width: SpacingTokens.lg), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(alert.title ?? '', style: TypographyTokens.bodyMedium.copyWith(fontWeight: FontWeight.w600, color: color)), Text(alert.message ?? '', style: TypographyTokens.bodySmall.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant)), ], ), ), if (alert.timestamp != null) Text(_formatTimestamp(alert.timestamp!), style: TypographyTokens.labelSmall.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant)), ], ), ); } /// Onglet Logs - Consultation détaillée Widget _buildLogsTab() { return Column( children: [ _buildLogsFilters(), Expanded(child: _buildLogsList()), ], ); } Widget _buildLogsFilters() { return Container( margin: const EdgeInsets.all(SpacingTokens.lg), padding: const EdgeInsets.all(SpacingTokens.xl), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(SpacingTokens.radiusXl), boxShadow: [ BoxShadow( color: AppColors.shadow, blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( children: [ Row( children: [ Expanded( child: Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(SpacingTokens.radiusLg), border: Border.all(color: Theme.of(context).colorScheme.outline), ), child: TextField( controller: _searchController, onChanged: (value) { setState(() => _searchQuery = value); _dispatchSearchLogs(); }, decoration: const InputDecoration( hintText: 'Rechercher dans les logs...', prefixIcon: Icon(Icons.search), border: InputBorder.none, contentPadding: EdgeInsets.symmetric(horizontal: SpacingTokens.xl, vertical: SpacingTokens.lg), ), ), ), ), const SizedBox(width: SpacingTokens.lg), Expanded(child: _buildFilterChip(_selectedLevel, _levels)), ], ), const SizedBox(height: SpacingTokens.lg), Row( children: [ Expanded(child: _buildFilterChip(_selectedTimeRange, _timeRanges)), const SizedBox(width: SpacingTokens.lg), Expanded(child: _buildFilterChip(_selectedSource, _sources)), const SizedBox(width: SpacingTokens.lg), ElevatedButton.icon( onPressed: () => _clearFilters(), style: ElevatedButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, foregroundColor: Theme.of(context).colorScheme.onSurfaceVariant, elevation: 0, ), icon: const Icon(Icons.clear, size: 16), label: const Text('Reset'), ), ], ), ], ), ); } Widget _buildFilterChip(String value, List options) { return Container( padding: const EdgeInsets.symmetric(horizontal: SpacingTokens.lg), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(SpacingTokens.radiusMd), border: Border.all(color: Theme.of(context).colorScheme.outline), ), child: DropdownButtonHideUnderline( child: DropdownButton( value: value, isExpanded: true, onChanged: (newValue) { setState(() { if (options == _levels) _selectedLevel = newValue!; if (options == _timeRanges) _selectedTimeRange = newValue!; if (options == _sources) _selectedSource = newValue!; }); _dispatchSearchLogs(); }, items: options.map((option) => DropdownMenuItem(value: option, child: Text(option))).toList(), ), ), ); } Widget _buildLogsList() { if (_isLoadingLogs) { return const Center( child: Padding( padding: EdgeInsets.all(SpacingTokens.xxxl), child: CircularProgressIndicator(), ), ); } if (_currentLogs.isEmpty) { return Center( child: Padding( padding: const EdgeInsets.all(SpacingTokens.xxxl), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.list_alt, size: 48, color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.4)), const SizedBox(height: SpacingTokens.lg), Text( 'Aucun log correspondant aux filtres', style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), ], ), ), ); } return ListView.builder( padding: const EdgeInsets.symmetric(horizontal: SpacingTokens.lg), itemCount: _currentLogs.length, itemBuilder: (context, index) => _buildLogEntry(_currentLogs[index]), ); } Widget _buildLogEntry(SystemLogModel log) { final color = _getLogColor(log.level ?? 'INFO'); final ts = log.timestamp; final timeStr = ts != null ? '${ts.hour.toString().padLeft(2, '0')}:${ts.minute.toString().padLeft(2, '0')}:${ts.second.toString().padLeft(2, '0')}' : '--:--:--'; return Container( margin: const EdgeInsets.only(bottom: SpacingTokens.md), padding: const EdgeInsets.all(SpacingTokens.lg), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(SpacingTokens.radiusLg), border: Border.all(color: color.withOpacity(0.2)), boxShadow: const [BoxShadow(color: AppColors.shadow, blurRadius: 4, offset: Offset(0, 2))], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: SpacingTokens.md, vertical: SpacingTokens.sm), decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(SpacingTokens.radiusLg)), child: Text( log.level ?? 'INFO', style: TypographyTokens.labelSmall.copyWith(color: AppColors.onPrimary, fontWeight: FontWeight.w600), ), ), const SizedBox(width: SpacingTokens.md), Text(timeStr, style: TypographyTokens.bodySmall.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant)), const Spacer(), if (log.source != null) Container( padding: const EdgeInsets.symmetric(horizontal: SpacingTokens.md, vertical: SpacingTokens.xs), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceVariant, borderRadius: BorderRadius.circular(SpacingTokens.radiusSm), ), child: Text(log.source!, style: TypographyTokens.labelSmall.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant, fontWeight: FontWeight.w600)), ), ], ), const SizedBox(height: SpacingTokens.md), Text(log.message ?? '', style: TypographyTokens.bodyMedium.copyWith(color: Theme.of(context).colorScheme.onSurface)), if (log.details != null && log.details!.isNotEmpty) ...[ const SizedBox(height: SpacingTokens.sm), Text(log.details!, style: TypographyTokens.bodySmall.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant)), ], if (log.username != null || log.ipAddress != null) ...[ const SizedBox(height: SpacingTokens.sm), Row( children: [ if (log.username != null) ...[ Icon(Icons.person_outline, size: 12, color: Theme.of(context).colorScheme.onSurfaceVariant), const SizedBox(width: SpacingTokens.xs), Text(log.username!, style: TypographyTokens.labelSmall.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant)), const SizedBox(width: SpacingTokens.lg), ], if (log.ipAddress != null) ...[ Icon(Icons.lan_outlined, size: 12, color: Theme.of(context).colorScheme.onSurfaceVariant), const SizedBox(width: SpacingTokens.xs), Text(log.ipAddress!, style: TypographyTokens.labelSmall.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant)), ], ], ), ], ], ), ); } /// Onglet 3 — Alertes (gestion uniquement, config déplacée dans Config) Widget _buildAlertsTab() { return SingleChildScrollView( padding: const EdgeInsets.all(SpacingTokens.lg), child: Column( children: [ const SizedBox(height: SpacingTokens.xl), _buildActiveAlerts(), const SizedBox(height: SpacingTokens.xl), _buildAlertHistoryInfo(), const SizedBox(height: 80), ], ), ); } Widget _buildActiveAlerts() { if (_currentAlerts.isEmpty) { return UFInfoCard( title: 'Alertes actives', icon: Icons.notifications_active, child: Padding( padding: const EdgeInsets.symmetric(vertical: SpacingTokens.xl), child: Column( children: [ Icon(Icons.check_circle_outline, size: 40, color: AppColors.success), const SizedBox(height: SpacingTokens.lg), Text('Aucune alerte active', style: TextStyle(color: AppColors.success, fontWeight: FontWeight.w600)), ], ), ), ); } return UFInfoCard( title: 'Alertes actives (${_currentAlerts.length})', icon: Icons.notifications_active, child: Column( children: _currentAlerts.map((alert) => _buildDetailedAlertItem(alert)).toList(), ), ); } Widget _buildDetailedAlertItem(SystemAlertModel alert) { final color = _getAlertColor(alert.level ?? 'INFO'); final isAcknowledged = alert.acknowledged ?? false; return Container( margin: const EdgeInsets.only(bottom: SpacingTokens.lg), padding: const EdgeInsets.all(SpacingTokens.lg), decoration: BoxDecoration( color: color.withOpacity(0.05), borderRadius: BorderRadius.circular(SpacingTokens.radiusLg), border: Border.all(color: isAcknowledged ? Theme.of(context).colorScheme.outlineVariant : color.withOpacity(0.3)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(_getAlertIcon(alert.level ?? 'INFO'), color: isAcknowledged ? Theme.of(context).colorScheme.onSurfaceVariant : color, size: 20), const SizedBox(width: SpacingTokens.md), Expanded( child: Text( alert.title ?? '', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: isAcknowledged ? Theme.of(context).colorScheme.onSurfaceVariant : color), ), ), if (!isAcknowledged && alert.id != null) ElevatedButton( onPressed: () => _acknowledgeAlert(alert.id!), style: ElevatedButton.styleFrom( backgroundColor: color, foregroundColor: AppColors.onPrimary, minimumSize: const Size(80, 32), padding: const EdgeInsets.symmetric(horizontal: SpacingTokens.lg), ), child: const Text('Acquitter', style: TextStyle(fontSize: 12)), ) else if (isAcknowledged) Container( padding: const EdgeInsets.symmetric(horizontal: SpacingTokens.md, vertical: SpacingTokens.xs), decoration: BoxDecoration( color: AppColors.successContainer, borderRadius: BorderRadius.circular(SpacingTokens.radiusSm), ), child: Text('Acquittée', style: TextStyle(fontSize: 11, color: AppColors.success, fontWeight: FontWeight.w600)), ), ], ), const SizedBox(height: SpacingTokens.md), Text(alert.message ?? '', style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant)), if (alert.timestamp != null) ...[ const SizedBox(height: SpacingTokens.sm), Text( 'Il y a ${_formatTimestamp(alert.timestamp!)}', style: TextStyle(fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant), ), ], if (alert.currentValue != null && alert.thresholdValue != null) ...[ const SizedBox(height: SpacingTokens.sm), Text( 'Valeur: ${alert.currentValue!.toStringAsFixed(1)} / Seuil: ${alert.thresholdValue!.toStringAsFixed(1)}', style: TextStyle(fontSize: 11, color: color, fontWeight: FontWeight.w600), ), ], ], ), ); } Widget _buildAlertHistoryInfo() { return UFInfoCard( title: 'Informations', icon: Icons.info_outline, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildInfoRow(Icons.tune, 'Configuration des alertes', 'Disponible dans l\'onglet Config', onTap: () => _tabController.animateTo(4)), const SizedBox(height: SpacingTokens.md), _buildInfoRow(Icons.history, 'Historique complet', 'Consultez l\'onglet Logs pour filtrer par niveau'), ], ), ); } Widget _buildInfoRow(IconData icon, String title, String subtitle, {VoidCallback? onTap}) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(SpacingTokens.radiusMd), child: Padding( padding: const EdgeInsets.symmetric(vertical: SpacingTokens.sm), child: Row( children: [ Icon(icon, size: 18, color: Theme.of(context).colorScheme.onSurfaceVariant), const SizedBox(width: SpacingTokens.lg), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: TextStyle(fontWeight: FontWeight.w600, fontSize: 13, color: Theme.of(context).colorScheme.onSurface)), Text(subtitle, style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant)), ], ), ), if (onTap != null) Icon(Icons.chevron_right, size: 16, color: Theme.of(context).colorScheme.onSurfaceVariant), ], ), ), ); } /// Onglet 4 — Métriques (réseau, perf applicative, base de données) Widget _buildMetricsTab() { return SingleChildScrollView( padding: const EdgeInsets.all(SpacingTokens.lg), child: Column( children: [ const SizedBox(height: SpacingTokens.xl), _buildNetworkMetrics(), const SizedBox(height: SpacingTokens.xl), _buildApplicationMetrics(), const SizedBox(height: SpacingTokens.xl), _buildDatabaseMetrics(), const SizedBox(height: 80), ], ), ); } Widget _buildNetworkMetrics() { return UFInfoCard( title: 'Réseau & Connexions', icon: Icons.network_check, child: Column( children: [ _buildMetricProgress('Trafic réseau', (_systemMetrics['network'] as num?)?.toDouble() ?? 0.0, ' MB/s', AppColors.info, maxValue: 100), const SizedBox(height: SpacingTokens.xl), Row( children: [ Expanded(child: _buildMetricValueCard('Connexions actives', '${_systemMetrics['activeConnections'] ?? 1247}', Icons.people_outline, AppColors.success)), const SizedBox(width: SpacingTokens.lg), Expanded(child: _buildMetricValueCard('Taux erreur', '${(((_systemMetrics['errorRate'] as num?)?.toDouble() ?? 0.02) * 100).toStringAsFixed(2)}%', Icons.error_outline, AppColors.error)), ], ), ], ), ); } Widget _buildApplicationMetrics() { return UFInfoCard( title: 'Performance applicative', icon: Icons.speed, child: Column( children: [ _buildMetricProgress('Temps de réponse', (_systemMetrics['responseTime'] as num? ?? 127).toDouble(), 'ms', AppColors.primaryDark, maxValue: 500), const SizedBox(height: SpacingTokens.xl), _buildMetricProgress('Taux d\'erreur', (((_systemMetrics['errorRate'] as num?)?.toDouble() ?? 0.02) * 100), '%', AppColors.warning, maxValue: 10), ], ), ); } Widget _buildDatabaseMetrics() { return UFInfoCard( title: 'Base de données', icon: Icons.storage, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded(child: _buildMetricValueCard('Pool connexions', 'Actif', Icons.check_circle_outline, AppColors.success)), const SizedBox(width: SpacingTokens.lg), Expanded(child: _buildMetricValueCard('Statut', 'OK', Icons.verified_outlined, AppColors.success)), ], ), const SizedBox(height: SpacingTokens.xl), _buildMetricProgress('Utilisation disque', (_systemMetrics['disk'] as num?)?.toDouble() ?? 45.8, '%', AppColors.warning, maxValue: 100), ], ), ); } Widget _buildMetricProgress(String label, double value, String unit, Color color, {double maxValue = 100}) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( label, style: TextStyle(fontWeight: FontWeight.w600, color: Theme.of(context).colorScheme.onSurface, fontSize: 13), ), Text( '${value.toStringAsFixed(1)}$unit', style: TextStyle(fontWeight: FontWeight.w600, color: color, fontSize: 13), ), ], ), const SizedBox(height: SpacingTokens.md), ClipRRect( borderRadius: BorderRadius.circular(SpacingTokens.radiusSm), child: LinearProgressIndicator( value: (value / maxValue).clamp(0.0, 1.0), backgroundColor: Theme.of(context).colorScheme.surfaceVariant, valueColor: AlwaysStoppedAnimation(color), minHeight: 6, ), ), ], ); } Widget _buildMetricValueCard(String label, String value, IconData icon, Color color) { return Container( padding: const EdgeInsets.all(SpacingTokens.lg), decoration: BoxDecoration( color: color.withOpacity(0.06), borderRadius: BorderRadius.circular(SpacingTokens.radiusLg), border: Border.all(color: color.withOpacity(0.15)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, color: color, size: 18), const SizedBox(height: SpacingTokens.sm), Text( value, style: TextStyle(fontWeight: FontWeight.bold, color: color, fontSize: 16), ), Text( label, style: TextStyle(fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant), ), ], ), ); } // Onglet "Config" retiré : toute la configuration (logs, alertes, monitoring) // est désormais dans Paramètres Système (Drawer → Système → Paramètres Système). // Logs & Monitoring est strictement orienté CONSULTATION (read-only). // ==================== MÉTHODES UTILITAIRES ==================== Color _getLogColor(String level) { switch (level) { case 'CRITICAL': return AppColors.accentDark; case 'ERROR': return ColorTokens.error; case 'WARN': return ColorTokens.warning; case 'INFO': return ColorTokens.info; case 'DEBUG': return ColorTokens.success; case 'TRACE': return ColorTokens.onSurfaceVariant; default: return ColorTokens.onSurfaceVariant; } } Color _getAlertColor(String level) { switch (level) { case 'CRITICAL': return AppColors.accentDark; case 'ERROR': return ColorTokens.error; case 'WARNING': return ColorTokens.warning; case 'INFO': return ColorTokens.info; default: return ColorTokens.onSurfaceVariant; } } IconData _getAlertIcon(String level) { switch (level) { case 'CRITICAL': return Icons.dangerous; case 'ERROR': return Icons.error; case 'WARNING': return Icons.warning; case 'INFO': return Icons.info; default: return Icons.notifications; } } String _formatTimestamp(DateTime timestamp) { final now = DateTime.now(); final difference = now.difference(timestamp); if (difference.inMinutes < 60) { return '${difference.inMinutes}min'; } else if (difference.inHours < 24) { return '${difference.inHours}h'; } else { return '${difference.inDays}j'; } } void _acknowledgeAlert(String alertId) { context.read().add(AcknowledgeAlert(alertId)); } void _clearFilters() { setState(() { _selectedLevel = 'Tous'; _selectedTimeRange = 'Dernières 24h'; _selectedSource = 'Tous'; _searchQuery = ''; _searchController.clear(); }); _showSuccessSnackBar('Filtres réinitialisés'); _dispatchSearchLogs(); } void _updateSystemMetricsFromState(dynamic metrics) { if (metrics == null) return; setState(() { _systemMetrics['cpu'] = metrics.cpuUsagePercent ?? 23.5; _systemMetrics['memory'] = metrics.memoryUsagePercent ?? 67.2; _systemMetrics['disk'] = metrics.diskUsagePercent ?? 45.8; _systemMetrics['network'] = metrics.networkUsageMbps ?? 12.3; _systemMetrics['activeConnections'] = metrics.activeConnections ?? 1247; _systemMetrics['errorRate'] = metrics.errorRate ?? 0.02; _systemMetrics['responseTime'] = metrics.averageResponseTimeMs ?? 127; _systemMetrics['uptime'] = metrics.uptimeFormatted ?? '15j 7h 23m'; // Nouvelles clés 24h — champs réels du SystemMetricsModel if (metrics.totalLogs24h != null) _systemMetrics['totalLogs'] = metrics.totalLogs24h; if (metrics.totalErrors24h != null) _systemMetrics['errors'] = metrics.totalErrors24h; if (metrics.totalWarnings24h != null) _systemMetrics['warnings'] = metrics.totalWarnings24h; }); } void _showSuccessSnackBar(String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), backgroundColor: ColorTokens.success, behavior: SnackBarBehavior.floating, ), ); } }