Files
Finlytic/FinlyticApp/lib/features/admin/widgets/system_diagnostics_widget.dart
T

284 lines
11 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../../core/network/api_client.dart';
import '../../../core/network/signalr_service.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/widgets/glass_container.dart';
import '../../../core/widgets/status_badge.dart';
import '../views/service_detail_screen.dart';
/// Real-Time System Diagnostics Widget driven EXCLUSIVELY over SignalR WebSockets (`/hubs/health`).
/// ZERO REST HTTP API calls are performed.
class SystemDiagnosticsWidget extends StatefulWidget {
final SignalRService? signalRService;
const SystemDiagnosticsWidget({
super.key,
this.signalRService,
});
@override
State<SystemDiagnosticsWidget> createState() => _SystemDiagnosticsWidgetState();
}
class _SystemDiagnosticsWidgetState extends State<SystemDiagnosticsWidget> {
List<Map<String, dynamic>> _serviceStatuses = [];
StreamSubscription<List<Map<String, dynamic>>>? _healthSub;
@override
void initState() {
super.initState();
if (widget.signalRService != null) {
_healthSub = widget.signalRService!.healthStream.listen((data) {
if (mounted) {
setState(() {
_serviceStatuses = data;
});
}
});
}
}
@override
void dispose() {
_healthSub?.cancel();
super.dispose();
}
IconData _getServiceIcon(String name) {
switch (name) {
case 'FinlyticBackend':
return Icons.hub_outlined;
case 'FinlyticAssets':
return Icons.inventory_2_outlined;
case 'FinlyticNews':
return Icons.newspaper_outlined;
case 'FinlyticTechnicals':
case 'FinlyticTechnicalAnalysis':
return Icons.show_chart_outlined;
case 'FinlyticSentiment':
return Icons.psychology_outlined;
case 'FinlyticEngine':
case 'FinlyticAnalyzer':
case 'FinlyticTrades':
return Icons.candlestick_chart_outlined;
case 'FinlyticFundamentals':
return Icons.corporate_fare_outlined;
case 'FinlyticBot':
return Icons.smart_toy_outlined;
default:
return Icons.dns_outlined;
}
}
@override
Widget build(BuildContext context) {
final int totalCount = _serviceStatuses.length;
final int onlineCount = _serviceStatuses.where((s) => s['status']?.toString().toLowerCase() == 'online').length;
final bool isWsConnected = widget.signalRService?.isConnected ?? false;
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// WebSocket Status Banner
GlassContainer(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: AppTheme.primaryEmerald.withValues(alpha: 0.15),
shape: BoxShape.circle,
border: Border.all(color: AppTheme.primaryEmerald.withValues(alpha: 0.3)),
),
child: Icon(Icons.sensors_rounded, color: AppTheme.primaryEmerald, size: 22),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text(
'SignalR WebSocket Live-Diagnose',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15),
),
const SizedBox(width: 8),
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: isWsConnected ? AppTheme.primaryEmerald : AppTheme.accentRed,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: (isWsConnected ? AppTheme.primaryEmerald : AppTheme.accentRed).withValues(alpha: 0.8),
blurRadius: 6,
spreadRadius: 2,
),
],
),
),
],
),
const SizedBox(height: 2),
Text(
'100% über SignalR WebSockets (/hubs/health). Keine HTTP API Anfragen.',
style: TextStyle(fontSize: 12, color: AppTheme.textSecondary),
),
],
),
),
const SizedBox(width: 10),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: (onlineCount == totalCount && totalCount > 0 ? AppTheme.primaryEmerald : AppTheme.accentCyan).withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: (onlineCount == totalCount && totalCount > 0 ? AppTheme.primaryEmerald : AppTheme.accentCyan).withValues(alpha: 0.4)),
),
child: Text(
totalCount > 0 ? '$onlineCount / $totalCount Online' : 'Verbinde WebSocket...',
style: TextStyle(
color: onlineCount == totalCount && totalCount > 0 ? AppTheme.primaryEmerald : AppTheme.accentCyan,
fontWeight: FontWeight.bold,
fontSize: 13,
),
),
),
],
),
),
const SizedBox(height: 16),
const Text(
'Echtzeit Dienststatus (SignalR Push)',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: 12),
if (_serviceStatuses.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: 40),
child: Center(
child: Column(
children: [
CircularProgressIndicator(color: AppTheme.primaryEmerald),
const SizedBox(height: 16),
Text('Warte auf SignalR WebSocket Daten von /hubs/health...', style: TextStyle(color: AppTheme.textMuted, fontSize: 13)),
],
),
),
)
else
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _serviceStatuses.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: MediaQuery.of(context).size.width > 900 ? 2 : 1,
childAspectRatio: 2.7,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemBuilder: (context, index) {
final svc = _serviceStatuses[index];
final String name = svc['name']?.toString() ?? 'Unbekannt';
final String type = svc['type']?.toString() ?? '';
final String status = svc['status']?.toString() ?? 'Offline';
final bool isOnline = status.toLowerCase() == 'online';
final String portInfo = svc['port']?.toString() ?? 'MQTT Only';
final String db = svc['db']?.toString() ?? 'PostgreSQL';
return InkWell(
onTap: () {
final apiClient = context.read<ApiClient>();
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ServiceDetailScreen(serviceName: name, apiClient: apiClient),
),
);
},
borderRadius: BorderRadius.circular(20),
child: GlassContainer(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(
_getServiceIcon(name),
size: 18,
color: isOnline ? AppTheme.primaryEmerald : AppTheme.accentRed,
),
const SizedBox(width: 8),
Text(
name,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
),
],
),
StatusBadge(
label: status,
color: isOnline ? AppTheme.primaryEmerald : AppTheme.accentRed,
),
],
),
Text(
type,
style: TextStyle(fontSize: 12, color: AppTheme.textSecondary),
overflow: TextOverflow.ellipsis,
),
const Divider(height: 10, color: Colors.white10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(Icons.storage_outlined, size: 12, color: AppTheme.textMuted),
const SizedBox(width: 4),
Text(
db,
style: TextStyle(fontSize: 11, color: AppTheme.textMuted),
),
],
),
Row(
children: [
Icon(name == 'FinlyticBackend' ? Icons.language_outlined : Icons.cable_outlined,
size: 12, color: AppTheme.accentCyan),
const SizedBox(width: 4),
Text(
portInfo,
style: TextStyle(
fontSize: 11,
color: name == 'FinlyticBackend' ? AppTheme.primaryEmerald : AppTheme.accentCyan,
fontWeight: FontWeight.w600,
),
),
],
),
],
),
],
),
),
);
},
),
],
),
);
}
}