Files
diagd/lib/widgets/live_monitor_tab.dart
T

546 lines
19 KiB
Dart

import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:diagd/src/rust/api/diagnostics.dart';
class LiveMonitorTab extends StatefulWidget {
const LiveMonitorTab({super.key});
@override
State<LiveMonitorTab> createState() => _LiveMonitorTabState();
}
class _LiveMonitorTabState extends State<LiveMonitorTab> {
Timer? _ticker;
LiveMetricsSnapshot? _snapshot;
final List<double> _cpuHistory = [];
final List<double> _ramHistory = [];
final List<double> _tempHistory = [];
final List<double> _netHistory = [];
final int _maxHistory = 60;
@override
void initState() {
super.initState();
_fetchMetrics();
_ticker = Timer.periodic(const Duration(seconds: 1), (_) => _fetchMetrics());
}
@override
void dispose() {
_ticker?.cancel();
super.dispose();
}
Future<void> _fetchMetrics() async {
try {
final snap = await getLiveMetrics();
if (!mounted) return;
setState(() {
_snapshot = snap;
_cpuHistory.add(snap.cpuUsagePercent.clamp(0.0, 100.0));
if (_cpuHistory.length > _maxHistory) _cpuHistory.removeAt(0);
final ramPct = snap.ramTotalBytes > BigInt.zero
? (snap.ramUsedBytes.toDouble() / snap.ramTotalBytes.toDouble()) * 100.0
: 0.0;
_ramHistory.add(ramPct.clamp(0.0, 100.0));
if (_ramHistory.length > _maxHistory) _ramHistory.removeAt(0);
_tempHistory.add(snap.maxCpuTempC.clamp(0.0, 120.0));
if (_tempHistory.length > _maxHistory) _tempHistory.removeAt(0);
final netKb = (snap.netRxKbps + snap.netTxKbps).toDouble();
_netHistory.add(netKb);
if (_netHistory.length > _maxHistory) _netHistory.removeAt(0);
});
} catch (_) {}
}
@override
Widget build(BuildContext context) {
if (_snapshot == null) {
return const Center(
child: CircularProgressIndicator(color: Color(0xFF58A6FF)),
);
}
final snap = _snapshot!;
final ramUsedGb = snap.ramUsedBytes.toDouble() / (1024 * 1024 * 1024);
final ramTotalGb = snap.ramTotalBytes.toDouble() / (1024 * 1024 * 1024);
final ramPct = ramTotalGb > 0 ? (ramUsedGb / ramTotalGb) * 100.0 : 0.0;
final swapUsedGb = snap.swapUsedBytes.toDouble() / (1024 * 1024 * 1024);
final swapTotalGb = snap.swapTotalBytes.toDouble() / (1024 * 1024 * 1024);
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Throttling Alert Banner
if (snap.isThrottling) ...[
Container(
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.only(bottom: 20),
decoration: BoxDecoration(
color: const Color(0xFFF85149).withAlpha(30),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFF85149)),
),
child: const Row(
children: [
Icon(Icons.warning_rounded, color: Color(0xFFF85149), size: 28),
SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'КРИТИЧЕСКИЙ ПЕРЕГРЕВ: АКТИВЕН THERMAL THROTTLING!',
style: TextStyle(
color: Color(0xFFF85149),
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
SizedBox(height: 4),
Text(
'Процессор сбросил тактовые частоты из-за превышения температурного порога. Проверьте систему охлаждения и термопасту.',
style: TextStyle(color: Color(0xFFE6EDF3), fontSize: 13),
),
],
),
),
],
),
),
],
// Top Metric Cards Grid
LayoutBuilder(
builder: (context, constraints) {
final cols = constraints.maxWidth > 900 ? 4 : (constraints.maxWidth > 550 ? 2 : 1);
return GridView.count(
crossAxisCount: cols,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisSpacing: 14,
mainAxisSpacing: 14,
childAspectRatio: constraints.maxWidth > 900 ? 1.6 : 2.0,
children: [
_buildMetricCard(
title: 'Загрузка CPU',
value: '${snap.cpuUsagePercent.toStringAsFixed(1)}%',
subvalue: 'Общая нагрузка ядер',
icon: Icons.memory_rounded,
color: snap.cpuUsagePercent > 85
? const Color(0xFFF85149)
: (snap.cpuUsagePercent > 60 ? const Color(0xFFD29922) : const Color(0xFF3FB950)),
progress: snap.cpuUsagePercent / 100.0,
),
_buildMetricCard(
title: 'Оперативная память (RAM)',
value: '${ramUsedGb.toStringAsFixed(1)} / ${ramTotalGb.toStringAsFixed(1)} GB',
subvalue: '${ramPct.toStringAsFixed(0)}% использовано',
icon: Icons.storage_rounded,
color: ramPct > 90
? const Color(0xFFF85149)
: (ramPct > 75 ? const Color(0xFFD29922) : const Color(0xFF58A6FF)),
progress: ramPct / 100.0,
),
_buildMetricCard(
title: 'Температура CPU',
value: '${snap.maxCpuTempC.toStringAsFixed(1)} °C',
subvalue: snap.isThrottling ? 'Троттлинг АКТИВЕН' : 'Охлаждение в норме',
icon: Icons.thermostat_rounded,
color: snap.maxCpuTempC > 85
? const Color(0xFFF85149)
: (snap.maxCpuTempC > 70 ? const Color(0xFFD29922) : const Color(0xFF3FB950)),
progress: (snap.maxCpuTempC.clamp(20.0, 105.0) - 20.0) / 85.0,
),
_buildMetricCard(
title: 'Дисковый и Сетевой I/O',
value: '${((snap.diskReadKbps + snap.diskWriteKbps).toDouble() / 1024.0).toStringAsFixed(1)} MB/s',
subvalue: 'Сеть: ${snap.netRxKbps + snap.netTxKbps} KB/s (RX+TX)',
icon: Icons.swap_vert_rounded,
color: const Color(0xFFA371F7),
progress: null,
),
],
);
},
),
const SizedBox(height: 24),
// Realtime Sparklines / Charts
const Text(
'ДИНАМИКА НАГРУЗКИ В РЕАЛЬНОМ ВРЕМЕНИ (ПОСЛЕДНИЕ 60 СЕК)',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
letterSpacing: 1.2,
color: Color(0xFF8B949E),
),
),
const SizedBox(height: 12),
LayoutBuilder(
builder: (context, constraints) {
final isWide = constraints.maxWidth > 800;
if (isWide) {
return Row(
children: [
Expanded(
child: _buildChartContainer(
title: 'Нагрузка CPU (%)',
color: const Color(0xFF58A6FF),
history: _cpuHistory,
maxVal: 100.0,
currentVal: '${snap.cpuUsagePercent.toStringAsFixed(1)}%',
),
),
const SizedBox(width: 14),
Expanded(
child: _buildChartContainer(
title: 'Использование RAM (%)',
color: const Color(0xFF3FB950),
history: _ramHistory,
maxVal: 100.0,
currentVal: '${ramPct.toStringAsFixed(1)}%',
),
),
],
);
} else {
return Column(
children: [
_buildChartContainer(
title: 'Нагрузка CPU (%)',
color: const Color(0xFF58A6FF),
history: _cpuHistory,
maxVal: 100.0,
currentVal: '${snap.cpuUsagePercent.toStringAsFixed(1)}%',
),
const SizedBox(height: 14),
_buildChartContainer(
title: 'Использование RAM (%)',
color: const Color(0xFF3FB950),
history: _ramHistory,
maxVal: 100.0,
currentVal: '${ramPct.toStringAsFixed(1)}%',
),
],
);
}
},
),
const SizedBox(height: 14),
LayoutBuilder(
builder: (context, constraints) {
final isWide = constraints.maxWidth > 800;
if (isWide) {
return Row(
children: [
Expanded(
child: _buildChartContainer(
title: 'Температура CPU (°C)',
color: const Color(0xFFD29922),
history: _tempHistory,
maxVal: 105.0,
currentVal: '${snap.maxCpuTempC.toStringAsFixed(1)}°C',
),
),
const SizedBox(width: 14),
Expanded(
child: _buildChartContainer(
title: 'Сетевой трафик (KB/s)',
color: const Color(0xFFA371F7),
history: _netHistory,
maxVal: _netHistory.isEmpty ? 100.0 : max(100.0, _netHistory.reduce(max)),
currentVal: '${snap.netRxKbps + snap.netTxKbps} KB/s',
),
),
],
);
} else {
return Column(
children: [
_buildChartContainer(
title: 'Температура CPU (°C)',
color: const Color(0xFFD29922),
history: _tempHistory,
maxVal: 105.0,
currentVal: '${snap.maxCpuTempC.toStringAsFixed(1)}°C',
),
const SizedBox(height: 14),
_buildChartContainer(
title: 'Сетевой трафик (KB/s)',
color: const Color(0xFFA371F7),
history: _netHistory,
maxVal: _netHistory.isEmpty ? 100.0 : max(100.0, _netHistory.reduce(max)),
currentVal: '${snap.netRxKbps + snap.netTxKbps} KB/s',
),
],
);
}
},
),
const SizedBox(height: 24),
// Detailed Memory & Storage Row
Container(
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFF30363D)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.info_outline_rounded, color: Color(0xFF58A6FF), size: 18),
SizedBox(width: 8),
Text(
'Детализация виртуальной памяти и I/O накопителей',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Color(0xFFE6EDF3),
fontSize: 14,
),
),
],
),
const SizedBox(height: 14),
Row(
children: [
Expanded(
child: _buildDetailRow(
'Swap память:',
'${swapUsedGb.toStringAsFixed(2)} GB из ${swapTotalGb.toStringAsFixed(2)} GB',
),
),
Expanded(
child: _buildDetailRow(
'Диск (Read / Write):',
'${(snap.diskReadKbps.toDouble() / 1024.0).toStringAsFixed(1)} MB/s / ${(snap.diskWriteKbps.toDouble() / 1024.0).toStringAsFixed(1)} MB/s',
),
),
Expanded(
child: _buildDetailRow(
'Сеть (RX / TX):',
'${snap.netRxKbps} KB/s / ${snap.netTxKbps} KB/s',
),
),
],
),
],
),
),
],
),
);
}
Widget _buildMetricCard({
required String title,
required String value,
required String subvalue,
required IconData icon,
required Color color,
double? progress,
}) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: color.withAlpha(50)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xFF8B949E),
),
),
Icon(icon, color: color, size: 20),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
value,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: color,
),
),
const SizedBox(height: 2),
Text(
subvalue,
style: const TextStyle(fontSize: 11, color: Color(0xFF8B949E)),
),
],
),
if (progress != null)
ClipRRect(
borderRadius: BorderRadius.circular(3),
child: LinearProgressIndicator(
value: progress.clamp(0.0, 1.0),
backgroundColor: const Color(0xFF21262D),
valueColor: AlwaysStoppedAnimation<Color>(color),
minHeight: 4,
),
)
else
const SizedBox(height: 4),
],
),
);
}
Widget _buildChartContainer({
required String title,
required Color color,
required List<double> history,
required double maxVal,
required String currentVal,
}) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFF30363D)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xFFE6EDF3),
),
),
Text(
currentVal,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: color,
),
),
],
),
const SizedBox(height: 12),
SizedBox(
height: 90,
width: double.infinity,
child: CustomPaint(
painter: _SparklinePainter(
data: history,
color: color,
maxVal: maxVal,
),
),
),
],
),
);
}
Widget _buildDetailRow(String label, String val) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: const TextStyle(fontSize: 11, color: Color(0xFF8B949E))),
const SizedBox(height: 3),
Text(val, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFFE6EDF3))),
],
);
}
}
class _SparklinePainter extends CustomPainter {
final List<double> data;
final Color color;
final double maxVal;
_SparklinePainter({
required this.data,
required this.color,
required this.maxVal,
});
@override
void paint(Canvas canvas, Size size) {
if (data.isEmpty) return;
final paintLine = Paint()
..color = color
..strokeWidth = 2.0
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
final paintFill = Paint()
..shader = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
color.withAlpha(80),
color.withAlpha(0),
],
).createShader(Rect.fromLTWH(0, 0, size.width, size.height))
..style = PaintingStyle.fill;
final path = Path();
final fillPath = Path();
final stepX = size.width / max(1, (60 - 1));
final startIdx = 60 - data.length;
for (int i = 0; i < data.length; i++) {
final x = (startIdx + i) * stepX;
final valNorm = (data[i] / (maxVal > 0 ? maxVal : 1.0)).clamp(0.0, 1.0);
final y = size.height - (valNorm * size.height);
if (i == 0) {
path.moveTo(x, y);
fillPath.moveTo(x, size.height);
fillPath.lineTo(x, y);
} else {
path.lineTo(x, y);
fillPath.lineTo(x, y);
}
}
fillPath.lineTo(size.width, size.height);
fillPath.close();
canvas.drawPath(fillPath, paintFill);
canvas.drawPath(path, paintLine);
}
@override
bool shouldRepaint(covariant _SparklinePainter oldDelegate) => true;
}