RefreshIndicator ajouté (dispatche les events BLoC appropriés) : - adhesion_detail, adhesions_page, demande_aide_detail, demandes_aide_page - event_detail, organization_detail, org_selector, org_types - user_management_detail, reports (TabBarView), logs (Dashboard tab) - profile (onglet Perso), backup (3 onglets), notifications Fixes associés : - AlwaysScrollableScrollPhysics sur tous les scroll widgets (permet pull-to-refresh même si contenu < écran) - Empty states des listes : wrappés dans SingleChildScrollView pour refresh - Dark mode adaptatif sur textes/surfaces/borders hardcodés - backup_page : bouton retour ajouté dans le header gradient - org_types : chevron/star/border adaptatifs - reports : couleurs placeholders graphique + chevrons
1214 lines
44 KiB
Dart
1214 lines
44 KiB
Dart
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<LogsMonitoringBloc>()..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<SystemLogModel> _currentLogs = [];
|
|
List<SystemAlertModel> _currentAlerts = [];
|
|
bool _isLoadingLogs = false;
|
|
|
|
// Données de configuration
|
|
final List<String> _levels = ['Tous', 'CRITICAL', 'ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE'];
|
|
final List<String> _timeRanges = ['Temps réel', 'Dernière heure', 'Dernières 24h', 'Dernière semaine', 'Dernier mois'];
|
|
final List<String> _sources = ['Tous', 'API', 'Auth', 'Database', 'Cache', 'Security', 'Performance', 'System'];
|
|
|
|
// Métriques système
|
|
final Map<String, dynamic> _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<LogsMonitoringBloc>()
|
|
..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<LogsMonitoringBloc>().add(LoadMetrics());
|
|
context.read<LogsMonitoringBloc>().add(LoadAlerts());
|
|
if (_isLiveMode) {
|
|
_dispatchSearchLogs();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return BlocConsumer<LogsMonitoringBloc, LogsMonitoringState>(
|
|
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<LogsMonitoringBloc>().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<LogsMonitoringBloc>()
|
|
..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<String> 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<String>(
|
|
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>(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<LogsMonitoringBloc>().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,
|
|
),
|
|
);
|
|
}
|
|
}
|