feat: initial commit of diagd - unified Linux/BSD/macOS/Windows diagnostic tool (TUI + GUI)

This commit is contained in:
2026-09-13 18:29:47 +03:00
commit c3a9569918
165 changed files with 31940 additions and 0 deletions
+486
View File
@@ -0,0 +1,486 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:diagd/src/rust/api/diagnostics.dart';
class CrashesTab extends StatefulWidget {
final List<CrashReportInfo> crashes;
const CrashesTab({
super.key,
required this.crashes,
});
@override
State<CrashesTab> createState() => _CrashesTabState();
}
class _CrashesTabState extends State<CrashesTab> {
int _selectedCrashIndex = 0;
@override
Widget build(BuildContext context) {
if (widget.crashes.isEmpty) {
return Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 550),
margin: const EdgeInsets.all(24),
padding: const EdgeInsets.all(32),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF238636).withAlpha(80)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF238636).withAlpha(30),
shape: BoxShape.circle,
),
child: const Icon(
Icons.verified_rounded,
color: Color(0xFF3FB950),
size: 44,
),
),
const SizedBox(height: 18),
const Text(
'Аварийных падений не зафиксировано',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFFE6EDF3),
),
),
const SizedBox(height: 8),
const Text(
'В системных журналах coredumpctl, /var/crash, dmesg и crash reporter отсутствуют записи о падениях процессов по сигналам SIGSEGV, SIGABRT или сбоям ядра.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 13, color: Color(0xFF8B949E), height: 1.4),
),
],
),
),
);
}
final selectedCrash = widget.crashes[_selectedCrashIndex.clamp(0, widget.crashes.length - 1)];
return LayoutBuilder(
builder: (context, constraints) {
final isWide = constraints.maxWidth > 850;
if (isWide) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Left Column: Crash list
SizedBox(
width: 340,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildListHeader(),
const SizedBox(height: 10),
Expanded(
child: ListView.separated(
itemCount: widget.crashes.length,
separatorBuilder: (context, index) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final crash = widget.crashes[index];
final isSelected = index == _selectedCrashIndex;
return _buildCrashListItem(crash, isSelected, () {
setState(() {
_selectedCrashIndex = index;
});
});
},
),
),
],
),
),
const SizedBox(width: 20),
// Right Column: Crash details
Expanded(
child: SingleChildScrollView(
child: _buildCrashDetails(context, selectedCrash),
),
),
],
),
);
} else {
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildListHeader(),
const SizedBox(height: 10),
SizedBox(
height: 180,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: widget.crashes.length,
separatorBuilder: (context, index) => const SizedBox(width: 10),
itemBuilder: (context, index) {
final crash = widget.crashes[index];
final isSelected = index == _selectedCrashIndex;
return SizedBox(
width: 260,
child: _buildCrashListItem(crash, isSelected, () {
setState(() {
_selectedCrashIndex = index;
});
}),
);
},
),
),
const SizedBox(height: 20),
_buildCrashDetails(context, selectedCrash),
],
),
);
}
},
);
}
Widget _buildListHeader() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'ЖУРНАЛ АВАРИЙНЫХ СБОЕВ',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
letterSpacing: 1.2,
color: Color(0xFF8B949E),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFFF85149).withAlpha(25),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFF85149).withAlpha(70)),
),
child: Text(
'${widget.crashes.length}',
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: Color(0xFFF85149),
),
),
),
],
);
}
Widget _buildCrashListItem(CrashReportInfo crash, bool isSelected, VoidCallback onTap) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF21262D) : const Color(0xFF161B22),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected ? const Color(0xFFF85149) : const Color(0xFF30363D),
width: isSelected ? 1.5 : 1.0,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFFF85149).withAlpha(30),
borderRadius: BorderRadius.circular(4),
),
child: Text(
crash.signalOrCode,
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: Color(0xFFF85149),
),
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
crash.processName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Color(0xFFE6EDF3),
),
),
),
],
),
const SizedBox(height: 6),
Text(
crash.timestamp,
style: const TextStyle(fontSize: 11, color: Color(0xFF8B949E)),
),
if (crash.pid != null) ...[
const SizedBox(height: 2),
Text(
'PID: ${crash.pid}',
style: const TextStyle(fontSize: 11, color: Color(0xFF8B949E), fontFamily: 'monospace'),
),
],
],
),
),
);
}
Widget _buildCrashDetails(BuildContext context, CrashReportInfo crash) {
final debugCmd = crash.dumpPath != null && crash.dumpPath!.isNotEmpty
? 'gdb ${crash.processName} ${crash.dumpPath!}'
: (crash.pid != null ? 'coredumpctl debug ${crash.pid}' : 'coredumpctl info ${crash.processName}');
return Container(
padding: const EdgeInsets.all(20),
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: [
Row(
children: [
const Icon(Icons.bug_report_rounded, color: Color(0xFFF85149), size: 24),
const SizedBox(width: 10),
Text(
crash.processName,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFFE6EDF3),
),
),
const SizedBox(width: 10),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: const Color(0xFFF85149).withAlpha(25),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: const Color(0xFFF85149).withAlpha(70)),
),
child: Text(
crash.signalOrCode,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Color(0xFFF85149),
),
),
),
],
),
if (crash.pid != null)
Text(
'PID: ${crash.pid}',
style: const TextStyle(
fontSize: 13,
color: Color(0xFF8B949E),
fontFamily: 'monospace',
),
),
],
),
const SizedBox(height: 16),
// Metadata Grid
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFF0D1117),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF21262D)),
),
child: Column(
children: [
_buildMetaRow('Время падения:', crash.timestamp),
if (crash.faultModule != null && crash.faultModule!.isNotEmpty)
_buildMetaRow('Сбойный модуль:', crash.faultModule!),
if (crash.dumpPath != null && crash.dumpPath!.isNotEmpty) ...[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
width: 130,
child: Text(
'Файл Core Dump:',
style: TextStyle(fontSize: 12, color: Color(0xFF8B949E)),
),
),
Expanded(
child: Text(
crash.dumpPath!,
style: const TextStyle(
fontSize: 12,
color: Color(0xFF58A6FF),
fontFamily: 'monospace',
),
),
),
IconButton(
icon: const Icon(Icons.copy_rounded, size: 14, color: Color(0xFF8B949E)),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
tooltip: 'Копировать путь к дампу',
onPressed: () {
Clipboard.setData(ClipboardData(text: crash.dumpPath!));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Путь к Core Dump скопирован'), duration: Duration(seconds: 2)),
);
},
),
],
),
],
],
),
),
const SizedBox(height: 20),
// Debug CLI Command
const Text(
'КОМАНДА ОТЛАДКИ (GDB / COREDUMPCTL)',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
letterSpacing: 1.0,
color: Color(0xFF8B949E),
),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFF0D1117),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF30363D)),
),
child: Row(
children: [
const Text(
'\$ ',
style: TextStyle(color: Color(0xFF3FB950), fontWeight: FontWeight.bold),
),
Expanded(
child: Text(
debugCmd,
style: const TextStyle(
color: Color(0xFFE6EDF3),
fontFamily: 'monospace',
fontSize: 13,
),
),
),
IconButton(
icon: const Icon(Icons.copy_rounded, size: 16, color: Color(0xFF8B949E)),
tooltip: 'Копировать команду',
onPressed: () {
Clipboard.setData(ClipboardData(text: debugCmd));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Команда отладки скопирована'), duration: Duration(seconds: 2)),
);
},
),
],
),
),
const SizedBox(height: 20),
// Stack Trace / Backtrace
const Text(
'СТЕК ВЫЗОВОВ (BACKTRACE / CRASH TRACE)',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
letterSpacing: 1.0,
color: Color(0xFF8B949E),
),
),
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFF0D1117),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF21262D)),
),
child: crash.stackTraceSnippet.isNotEmpty
? SelectableText(
crash.stackTraceSnippet,
style: const TextStyle(
color: Color(0xFFE6EDF3),
fontFamily: 'monospace',
fontSize: 12,
height: 1.4,
),
)
: const Text(
'Трассировка стека недоступна в данном дампе памяти.',
style: TextStyle(color: Color(0xFF8B949E), fontSize: 12),
),
),
],
),
);
}
Widget _buildMetaRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 130,
child: Text(
label,
style: const TextStyle(fontSize: 12, color: Color(0xFF8B949E)),
),
),
Expanded(
child: Text(
value,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFFE6EDF3),
),
),
),
],
),
);
}
}
File diff suppressed because it is too large Load Diff
+545
View File
@@ -0,0 +1,545 @@
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;
}
+497
View File
@@ -0,0 +1,497 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:diagd/src/rust/api/diagnostics.dart';
class SecurityTab extends StatelessWidget {
final SecurityAuditReport security;
const SecurityTab({
super.key,
required this.security,
});
@override
Widget build(BuildContext context) {
final score = security.securityScore;
final scoreColor = score >= 80
? const Color(0xFF3FB950)
: (score >= 50 ? const Color(0xFFD29922) : const Color(0xFFF85149));
final failedChecks = security.hardeningChecks.where((c) => !c.isSecure).length;
final passedChecks = security.hardeningChecks.where((c) => c.isSecure).length;
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Security Score Hero Card
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: scoreColor.withAlpha(70)),
),
child: Row(
children: [
Stack(
alignment: Alignment.center,
children: [
SizedBox(
width: 80,
height: 80,
child: CircularProgressIndicator(
value: score / 100.0,
strokeWidth: 8,
backgroundColor: const Color(0xFF21262D),
valueColor: AlwaysStoppedAnimation<Color>(scoreColor),
),
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'$score',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: scoreColor,
),
),
const Text(
'/100',
style: TextStyle(fontSize: 10, color: Color(0xFF8B949E)),
),
],
),
],
),
const SizedBox(width: 24),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'Индекс защищенности системы (Security Score)',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFFE6EDF3),
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: scoreColor.withAlpha(30),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: scoreColor.withAlpha(80)),
),
child: Text(
score >= 80 ? 'ОТЛИЧНО' : (score >= 50 ? 'ТРЕБУЕТ ВНИМАНИЯ' : 'УЯЗВИМО'),
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: scoreColor,
),
),
),
],
),
const SizedBox(height: 6),
Text(
'Аудит сетевого периметра, прав суперпользователя SUID/SGID, параметров защиты ядра (ASLR, ptrace, eBPF) и конфигурации SSH.',
style: const TextStyle(fontSize: 13, color: Color(0xFF8B949E)),
),
const SizedBox(height: 12),
Row(
children: [
_buildMiniBadge('Открытых портов (0.0.0.0): ${security.exposedPorts.length}',
security.exposedPorts.isNotEmpty ? const Color(0xFFD29922) : const Color(0xFF3FB950)),
const SizedBox(width: 10),
_buildMiniBadge('SUID аномалий: ${security.suidAnomalies.length}',
security.suidAnomalies.isNotEmpty ? const Color(0xFFF85149) : const Color(0xFF3FB950)),
const SizedBox(width: 10),
_buildMiniBadge('Пройдено тестов: $passedChecks / ${security.hardeningChecks.length}',
failedChecks > 0 ? const Color(0xFFD29922) : const Color(0xFF3FB950)),
],
),
],
),
),
],
),
),
const SizedBox(height: 24),
// 1. Exposed Listening Ports
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'СЕТЕВОЙ ПЕРИМЕТР (ПОРТЫ, СЛУШАЮЩИЕ 0.0.0.0 И ::)',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
letterSpacing: 1.2,
color: Color(0xFF8B949E),
),
),
Text(
'Всего: ${security.exposedPorts.length}',
style: const TextStyle(fontSize: 12, color: Color(0xFF8B949E)),
),
],
),
const SizedBox(height: 10),
if (security.exposedPorts.isEmpty)
_buildEmptyState('Открытых вовне портов не обнаружено. Локальный сетевой стек защищен.', Icons.verified_user_rounded)
else
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: security.exposedPorts.length,
separatorBuilder: (context, index) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final port = security.exposedPorts[index];
return _buildPortCard(context, port);
},
),
const SizedBox(height: 28),
// 2. SUID / SGID Anomalies
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'ПОДОЗРИТЕЛЬНЫЕ SUID / SGID БИНАРНЫЕ ФАЙЛЫ',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
letterSpacing: 1.2,
color: Color(0xFF8B949E),
),
),
Text(
'Аномалий: ${security.suidAnomalies.length}',
style: const TextStyle(fontSize: 12, color: Color(0xFF8B949E)),
),
],
),
const SizedBox(height: 10),
if (security.suidAnomalies.isEmpty)
_buildEmptyState('Аномальных или небезопасных SUID/SGID файлов в общедоступных каталогах (/tmp, /dev/shm) не обнаружено.', Icons.check_circle_outline_rounded)
else
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: security.suidAnomalies.length,
separatorBuilder: (context, index) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final suid = security.suidAnomalies[index];
return _buildSuidCard(suid);
},
),
const SizedBox(height: 28),
// 3. Kernel & OS Hardening Checklist
const Text(
'ЧЕК-ЛИСТ ЗАЩИТЫ ЯДРА И ОПЕРАЦИОННОЙ СИСТЕМЫ (HARDENING)',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
letterSpacing: 1.2,
color: Color(0xFF8B949E),
),
),
const SizedBox(height: 10),
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: security.hardeningChecks.length,
separatorBuilder: (context, index) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final check = security.hardeningChecks[index];
return _buildHardeningCard(context, check);
},
),
],
),
);
}
Widget _buildMiniBadge(String text, Color color) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: color.withAlpha(20),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: color.withAlpha(60)),
),
child: Text(
text,
style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: color),
),
);
}
Widget _buildEmptyState(String text, IconData icon) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF238636).withAlpha(50)),
),
child: Row(
children: [
Icon(icon, color: const Color(0xFF3FB950), size: 22),
const SizedBox(width: 12),
Expanded(
child: Text(
text,
style: const TextStyle(fontSize: 13, color: Color(0xFF8B949E)),
),
),
],
),
);
}
Widget _buildPortCard(BuildContext context, ListeningPortInfo port) {
final (Color badgeColor, String badgeText) = switch (port.riskLevel) {
Severity.critical => (const Color(0xFFF85149), 'CRITICAL'),
Severity.warning => (const Color(0xFFD29922), 'WARNING'),
Severity.info => (const Color(0xFF3FB950), 'INFO'),
};
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: badgeColor.withAlpha(60)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: badgeColor.withAlpha(25),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: badgeColor.withAlpha(80)),
),
child: Text(
badgeText,
style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: badgeColor),
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'${port.protocol.toUpperCase()} ${port.port}',
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: Color(0xFFE6EDF3),
),
),
const SizedBox(width: 10),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
decoration: BoxDecoration(
color: const Color(0xFF21262D),
borderRadius: BorderRadius.circular(4),
),
child: Text(
port.processName.isNotEmpty ? port.processName : 'unknown process',
style: const TextStyle(fontSize: 12, color: Color(0xFF58A6FF), fontFamily: 'monospace'),
),
),
if (port.pid != null) ...[
const SizedBox(width: 6),
Text('(PID: ${port.pid})', style: const TextStyle(fontSize: 11, color: Color(0xFF8B949E))),
],
],
),
const SizedBox(height: 4),
Text(
'Адрес прослушивания: ${port.localAddress}',
style: const TextStyle(fontSize: 12, color: Color(0xFF8B949E)),
),
if (port.recommendation.isNotEmpty) ...[
const SizedBox(height: 6),
Row(
children: [
const Icon(Icons.arrow_right_rounded, size: 18, color: Color(0xFFD29922)),
Expanded(
child: Text(
port.recommendation,
style: TextStyle(fontSize: 12, color: badgeColor),
),
),
],
),
],
],
),
),
],
),
);
}
Widget _buildSuidCard(SuidAnomalyInfo suid) {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFF85149).withAlpha(60)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.warning_amber_rounded, color: Color(0xFFF85149), size: 22),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
suid.path,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Color(0xFFE6EDF3),
fontFamily: 'monospace',
),
),
const SizedBox(height: 4),
Text(
'Владелец: ${suid.owner} | Права: ${suid.permissions}',
style: const TextStyle(fontSize: 12, color: Color(0xFF8B949E)),
),
const SizedBox(height: 4),
Text(
suid.riskReason,
style: const TextStyle(fontSize: 12, color: Color(0xFFF85149)),
),
],
),
),
],
),
);
}
Widget _buildHardeningCard(BuildContext context, SecurityCheckItem check) {
final isPass = check.isSecure;
final color = isPass ? const Color(0xFF3FB950) : const Color(0xFFF85149);
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withAlpha(50)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
isPass ? Icons.check_circle_rounded : Icons.cancel_rounded,
color: color,
size: 20,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
check.name,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Color(0xFFE6EDF3),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: color.withAlpha(20),
borderRadius: BorderRadius.circular(6),
),
child: Text(
check.status,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: color,
),
),
),
],
),
const SizedBox(height: 4),
Text(
check.description,
style: const TextStyle(fontSize: 12, color: Color(0xFF8B949E)),
),
if (!isPass && check.remediation != null && check.remediation!.isNotEmpty) ...[
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFF0D1117),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: const Color(0xFF30363D)),
),
child: Row(
children: [
const Icon(Icons.build_rounded, size: 14, color: Color(0xFFD29922)),
const SizedBox(width: 8),
Expanded(
child: Text(
check.remediation!,
style: const TextStyle(fontSize: 12, color: Color(0xFFE6EDF3), fontFamily: 'monospace'),
),
),
IconButton(
icon: const Icon(Icons.copy_rounded, size: 16, color: Color(0xFF8B949E)),
tooltip: 'Скопировать команду',
onPressed: () {
Clipboard.setData(ClipboardData(text: check.remediation!));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Команда устранения скопирована в буфер'),
duration: Duration(seconds: 2),
),
);
},
),
],
),
),
],
],
),
),
],
),
);
}
}