546 lines
19 KiB
Dart
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;
|
|
}
|