1079 lines
37 KiB
Dart
1079 lines
37 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:diagd/src/rust/api/diagnostics.dart';
|
|
import 'package:diagd/widgets/hardware_tab.dart';
|
|
import 'package:diagd/widgets/live_monitor_tab.dart';
|
|
import 'package:diagd/widgets/security_tab.dart';
|
|
import 'package:diagd/widgets/crashes_tab.dart';
|
|
|
|
class DashboardScreen extends StatefulWidget {
|
|
const DashboardScreen({super.key});
|
|
|
|
@override
|
|
State<DashboardScreen> createState() => _DashboardScreenState();
|
|
}
|
|
|
|
class _DashboardScreenState extends State<DashboardScreen> {
|
|
bool _isLoading = false;
|
|
bool _isExporting = false;
|
|
int _selectedTab = 0;
|
|
SystemReport? _report;
|
|
String? _errorMessage;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_performScan();
|
|
}
|
|
|
|
Future<void> _performScan() async {
|
|
setState(() {
|
|
_isLoading = true;
|
|
_errorMessage = null;
|
|
});
|
|
|
|
try {
|
|
final report = await scanSystemAndAnalyze();
|
|
setState(() {
|
|
_report = report;
|
|
_isLoading = false;
|
|
});
|
|
} catch (e) {
|
|
setState(() {
|
|
_errorMessage = e.toString();
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _exportDebugBundle() async {
|
|
if (_report == null) return;
|
|
setState(() {
|
|
_isExporting = true;
|
|
});
|
|
|
|
try {
|
|
final path = await exportDebugBundle(report: _report!);
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Row(
|
|
children: [
|
|
const Icon(Icons.check_circle_rounded, color: Color(0xFF3FB950), size: 20),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: Text(
|
|
'Debug Bundle экспортирован: $path',
|
|
style: const TextStyle(fontSize: 13),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
backgroundColor: const Color(0xFF161B22),
|
|
duration: const Duration(seconds: 5),
|
|
action: SnackBarAction(
|
|
label: 'Копировать путь',
|
|
textColor: const Color(0xFF58A6FF),
|
|
onPressed: () {
|
|
Clipboard.setData(ClipboardData(text: path));
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Ошибка экспорта: $e'),
|
|
backgroundColor: const Color(0xFFF85149),
|
|
),
|
|
);
|
|
}
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isExporting = false;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: const Color(0xFF0D1117),
|
|
appBar: _buildAppBar(),
|
|
body: _buildBody(),
|
|
);
|
|
}
|
|
|
|
PreferredSizeWidget _buildAppBar() {
|
|
return AppBar(
|
|
backgroundColor: const Color(0xFF161B22),
|
|
elevation: 0,
|
|
title: Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF238636).withAlpha(40),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: const Color(0xFF238636).withAlpha(80)),
|
|
),
|
|
child: const Icon(
|
|
Icons.terminal_rounded,
|
|
color: Color(0xFF3FB950),
|
|
size: 22,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'diagd • Linux AI Diagnostic Analyst',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w700,
|
|
color: Color(0xFFE6EDF3),
|
|
letterSpacing: 0.2,
|
|
),
|
|
),
|
|
Text(
|
|
_report != null
|
|
? 'Последнее сканирование: ${_report!.timestamp}'
|
|
: 'Готов к диагностике системы',
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
color: Color(0xFF8B949E),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
if (_report != null && _report!.promptPreview.isNotEmpty)
|
|
TextButton.icon(
|
|
onPressed: () => _showPromptModal(context, _report!.promptPreview),
|
|
icon: const Icon(Icons.psychology_outlined, size: 18, color: Color(0xFF58A6FF)),
|
|
label: const Text(
|
|
'LLM Prompt',
|
|
style: TextStyle(color: Color(0xFF58A6FF), fontSize: 13),
|
|
),
|
|
),
|
|
if (_report != null) ...[
|
|
const SizedBox(width: 4),
|
|
TextButton.icon(
|
|
onPressed: _isExporting ? null : _exportDebugBundle,
|
|
icon: _isExporting
|
|
? const SizedBox(
|
|
width: 14,
|
|
height: 14,
|
|
child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF3FB950)),
|
|
)
|
|
: const Icon(Icons.archive_outlined, size: 18, color: Color(0xFF3FB950)),
|
|
label: Text(
|
|
_isExporting ? 'Экспорт...' : 'Экспорт отчета',
|
|
style: const TextStyle(color: Color(0xFF3FB950), fontSize: 13),
|
|
),
|
|
),
|
|
],
|
|
const SizedBox(width: 8),
|
|
Padding(
|
|
padding: const EdgeInsets.only(right: 16),
|
|
child: ElevatedButton.icon(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF238636),
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
onPressed: _isLoading ? null : _performScan,
|
|
icon: _isLoading
|
|
? const SizedBox(
|
|
width: 16,
|
|
height: 16,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
color: Colors.white,
|
|
),
|
|
)
|
|
: const Icon(Icons.refresh_rounded, size: 18),
|
|
label: Text(
|
|
_isLoading ? 'Анализ...' : 'Начать сканирование',
|
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
bottom: _report != null
|
|
? PreferredSize(
|
|
preferredSize: const Size.fromHeight(48),
|
|
child: Container(
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xFF161B22),
|
|
border: Border(bottom: BorderSide(color: Color(0xFF30363D), width: 1)),
|
|
),
|
|
child: SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: Row(
|
|
children: [
|
|
const SizedBox(width: 24),
|
|
_buildTabButton(
|
|
index: 0,
|
|
title: 'Диагностика',
|
|
badge: _report!.issues.isNotEmpty ? '${_report!.issues.length}' : null,
|
|
icon: Icons.troubleshoot_rounded,
|
|
),
|
|
const SizedBox(width: 6),
|
|
_buildTabButton(
|
|
index: 1,
|
|
title: 'Оборудование',
|
|
icon: Icons.memory_rounded,
|
|
),
|
|
const SizedBox(width: 6),
|
|
_buildTabButton(
|
|
index: 2,
|
|
title: 'Live Монитор',
|
|
icon: Icons.speed_rounded,
|
|
),
|
|
const SizedBox(width: 6),
|
|
_buildTabButton(
|
|
index: 3,
|
|
title: 'Безопасность',
|
|
badge: '${_report!.security.securityScore}%',
|
|
icon: Icons.security_rounded,
|
|
),
|
|
const SizedBox(width: 6),
|
|
_buildTabButton(
|
|
index: 4,
|
|
title: 'Аварии и Сбои',
|
|
badge: _report!.crashes.isNotEmpty ? '${_report!.crashes.length}' : null,
|
|
icon: Icons.bug_report_rounded,
|
|
),
|
|
const SizedBox(width: 24),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
)
|
|
: null,
|
|
);
|
|
}
|
|
|
|
Widget _buildTabButton({
|
|
required int index,
|
|
required String title,
|
|
required IconData icon,
|
|
String? badge,
|
|
}) {
|
|
final isSelected = _selectedTab == index;
|
|
return InkWell(
|
|
onTap: () {
|
|
setState(() {
|
|
_selectedTab = index;
|
|
});
|
|
},
|
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(8)),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
border: Border(
|
|
bottom: BorderSide(
|
|
color: isSelected ? const Color(0xFFF78166) : Colors.transparent,
|
|
width: 2,
|
|
),
|
|
),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
icon,
|
|
size: 16,
|
|
color: isSelected ? const Color(0xFFE6EDF3) : const Color(0xFF8B949E),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
|
color: isSelected ? const Color(0xFFE6EDF3) : const Color(0xFF8B949E),
|
|
),
|
|
),
|
|
if (badge != null) ...[
|
|
const SizedBox(width: 8),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF30363D),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Text(
|
|
badge,
|
|
style: const TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w600,
|
|
color: Color(0xFFE6EDF3),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildBody() {
|
|
if (_isLoading && _report == null) {
|
|
return const Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
CircularProgressIndicator(color: Color(0xFF58A6FF)),
|
|
SizedBox(height: 20),
|
|
Text(
|
|
'Опрос systemd, чтение journalctl / dmesg и санитизация логов...',
|
|
style: TextStyle(color: Color(0xFF8B949E), fontSize: 14),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
if (_errorMessage != null && _report == null) {
|
|
return Center(
|
|
child: Container(
|
|
constraints: const BoxConstraints(maxWidth: 600),
|
|
margin: const EdgeInsets.all(24),
|
|
padding: const EdgeInsets.all(24),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF21262D),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: const Color(0xFFF85149).withAlpha(100)),
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Icon(Icons.error_outline_rounded, color: Color(0xFFF85149), size: 48),
|
|
const SizedBox(height: 16),
|
|
const Text(
|
|
'Ошибка сканирования системы',
|
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
_errorMessage!,
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(color: Color(0xFF8B949E), fontFamily: 'monospace'),
|
|
),
|
|
const SizedBox(height: 20),
|
|
ElevatedButton(
|
|
onPressed: _performScan,
|
|
child: const Text('Повторить'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (_report == null) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
|
|
if (_selectedTab == 1) {
|
|
return HardwareTab(hardware: _report!.hardware);
|
|
} else if (_selectedTab == 2) {
|
|
return const LiveMonitorTab();
|
|
} else if (_selectedTab == 3) {
|
|
return SecurityTab(security: _report!.security);
|
|
} else if (_selectedTab == 4) {
|
|
return CrashesTab(crashes: _report!.crashes);
|
|
}
|
|
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildSummaryBanner(_report!),
|
|
const SizedBox(height: 24),
|
|
const Text(
|
|
'СОСТОЯНИЕ ПОДСИСТЕМ',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 1.2,
|
|
color: Color(0xFF8B949E),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
_buildSubsystemsGrid(_report!.subsystems),
|
|
const SizedBox(height: 28),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'НАЙДЕННЫЕ ИНЦИДЕНТЫ (${_report!.issues.length})',
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 1.2,
|
|
color: Color(0xFF8B949E),
|
|
),
|
|
),
|
|
if (_report!.llmAnalyzed)
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF1F6FEB).withAlpha(40),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: const Color(0xFF58A6FF)),
|
|
),
|
|
child: const Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.auto_awesome, size: 14, color: Color(0xFF58A6FF)),
|
|
SizedBox(width: 6),
|
|
Text(
|
|
'Обработано LLM',
|
|
style: TextStyle(fontSize: 11, color: Color(0xFF58A6FF), fontWeight: FontWeight.w600),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
_buildIssuesList(_report!.issues),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSummaryBanner(SystemReport report) {
|
|
final criticalCount = report.issues.where((i) => i.severity == Severity.critical).length;
|
|
final warningCount = report.issues.where((i) => i.severity == Severity.warning).length;
|
|
|
|
final Color statusColor = criticalCount > 0
|
|
? const Color(0xFFF85149)
|
|
: (warningCount > 0 ? const Color(0xFFD29922) : const Color(0xFF3FB950));
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(18),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF161B22),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: statusColor.withAlpha(80)),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: statusColor.withAlpha(30),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(
|
|
criticalCount > 0
|
|
? Icons.warning_amber_rounded
|
|
: (warningCount > 0 ? Icons.info_outline_rounded : Icons.check_circle_outline_rounded),
|
|
color: statusColor,
|
|
size: 28,
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
criticalCount > 0
|
|
? 'Обнаружены критические сбои системы'
|
|
: (warningCount > 0 ? 'Обнаружены предупреждения' : 'Система полностью стабильна'),
|
|
style: const TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: Color(0xFFE6EDF3),
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
report.summary,
|
|
style: const TextStyle(fontSize: 13, color: Color(0xFF8B949E)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Row(
|
|
children: [
|
|
_buildStatChip('Критические', criticalCount, const Color(0xFFF85149)),
|
|
const SizedBox(width: 8),
|
|
_buildStatChip('Предупреждения', warningCount, const Color(0xFFD29922)),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStatChip(String label, int count, Color color) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: color.withAlpha(25),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: color.withAlpha(70)),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Text(
|
|
count.toString(),
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: color,
|
|
),
|
|
),
|
|
Text(
|
|
label,
|
|
style: const TextStyle(fontSize: 10, color: Color(0xFF8B949E)),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSubsystemsGrid(List<SubsystemStatus> subsystems) {
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final crossAxisCount = constraints.maxWidth > 900 ? 5 : (constraints.maxWidth > 550 ? 3 : 2);
|
|
return GridView.builder(
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
itemCount: subsystems.length,
|
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
|
crossAxisCount: crossAxisCount,
|
|
mainAxisExtent: 110,
|
|
crossAxisSpacing: 12,
|
|
mainAxisSpacing: 12,
|
|
),
|
|
itemBuilder: (context, index) {
|
|
final sub = subsystems[index];
|
|
return _buildSubsystemCard(sub);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildSubsystemCard(SubsystemStatus sub) {
|
|
final (IconData icon, String name) = switch (sub.name) {
|
|
Subsystem.systemd => (Icons.settings_suggest_rounded, 'Systemd'),
|
|
Subsystem.kernel => (Icons.memory_rounded, 'Kernel'),
|
|
Subsystem.storage => (Icons.storage_rounded, 'Storage'),
|
|
Subsystem.audio => (Icons.volume_up_rounded, 'Audio'),
|
|
Subsystem.network => (Icons.lan_rounded, 'Network'),
|
|
Subsystem.security => (Icons.security_rounded, 'Security'),
|
|
Subsystem.crashes => (Icons.bug_report_rounded, 'Crashes'),
|
|
};
|
|
|
|
final isHealthy = sub.status == 'Healthy';
|
|
final isFailed = sub.status == 'Failed';
|
|
final color = isFailed
|
|
? const Color(0xFFF85149)
|
|
: (isHealthy ? const Color(0xFF3FB950) : const Color(0xFFD29922));
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF161B22),
|
|
borderRadius: BorderRadius.circular(10),
|
|
border: Border.all(color: color.withAlpha(60)),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Icon(icon, color: const Color(0xFFE6EDF3), size: 20),
|
|
Container(
|
|
width: 8,
|
|
height: 8,
|
|
decoration: BoxDecoration(
|
|
color: color,
|
|
shape: BoxShape.circle,
|
|
boxShadow: [
|
|
BoxShadow(color: color.withAlpha(120), blurRadius: 4, spreadRadius: 1),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
name,
|
|
style: const TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.bold,
|
|
color: Color(0xFFE6EDF3),
|
|
),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
sub.status,
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w600,
|
|
color: color,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildIssuesList(List<IssueItem> issues) {
|
|
if (issues.isEmpty) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(32),
|
|
width: double.infinity,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF161B22),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: const Center(
|
|
child: Text(
|
|
'Проблем не обнаружено',
|
|
style: TextStyle(color: Color(0xFF8B949E)),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
return ListView.separated(
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
itemCount: issues.length,
|
|
separatorBuilder: (_, _) => const SizedBox(height: 16),
|
|
itemBuilder: (context, index) {
|
|
return _buildIssueCard(issues[index]);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildIssueCard(IssueItem issue) {
|
|
final (badgeColor, badgeText, badgeIcon) = switch (issue.severity) {
|
|
Severity.critical => (
|
|
const Color(0xFFF85149),
|
|
'CRITICAL',
|
|
Icons.error_outline_rounded,
|
|
),
|
|
Severity.warning => (
|
|
const Color(0xFFD29922),
|
|
'WARNING',
|
|
Icons.warning_amber_rounded,
|
|
),
|
|
Severity.info => (
|
|
const Color(0xFF58A6FF),
|
|
'INFO',
|
|
Icons.info_outline_rounded,
|
|
),
|
|
};
|
|
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF161B22),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: const Color(0xFF30363D)),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Верхняя плашка: Бейдж критичности + Подсистема
|
|
Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
|
decoration: BoxDecoration(
|
|
color: badgeColor.withAlpha(35),
|
|
borderRadius: BorderRadius.circular(6),
|
|
border: Border.all(color: badgeColor.withAlpha(90)),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(badgeIcon, size: 13, color: badgeColor),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
badgeText,
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w700,
|
|
color: badgeColor,
|
|
letterSpacing: 0.5,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF21262D),
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: Text(
|
|
issue.subsystem.name.toUpperCase(),
|
|
style: const TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w600,
|
|
color: Color(0xFF8B949E),
|
|
),
|
|
),
|
|
),
|
|
const Spacer(),
|
|
if (issue.sanitized)
|
|
const Tooltip(
|
|
message: 'Логи очищены от персональных путей, IP и токенов',
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.shield_outlined, size: 14, color: Color(0xFF3FB950)),
|
|
SizedBox(width: 4),
|
|
Text(
|
|
'Sanitized',
|
|
style: TextStyle(fontSize: 11, color: Color(0xFF3FB950)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
// Заголовок проблемы
|
|
Text(
|
|
issue.title,
|
|
style: const TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w700,
|
|
color: Color(0xFFE6EDF3),
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
// Корень проблемы (Root Cause)
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF0D1117),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: const Color(0xFF21262D)),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'КОРЕНЬ ПРОБЛЕМЫ (ROOT CAUSE)',
|
|
style: TextStyle(
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 0.8,
|
|
color: Color(0xFF58A6FF),
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
issue.rootCause,
|
|
style: const TextStyle(
|
|
fontSize: 13,
|
|
color: Color(0xFFC9D1D9),
|
|
height: 1.4,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
issue.explanation,
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
color: Color(0xFF8B949E),
|
|
height: 1.3,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
// Блок с решением / командой
|
|
if (issue.fix != null) ...[
|
|
const SizedBox(height: 14),
|
|
_buildFixCommandSection(issue.fix!),
|
|
],
|
|
const SizedBox(height: 14),
|
|
// Кнопка показа сырого лога
|
|
Align(
|
|
alignment: Alignment.centerRight,
|
|
child: TextButton.icon(
|
|
onPressed: () => _showRawLogModal(context, issue.title, issue.rawLog),
|
|
icon: const Icon(Icons.code_rounded, size: 16, color: Color(0xFF8B949E)),
|
|
label: const Text(
|
|
'Показать сырой лог',
|
|
style: TextStyle(fontSize: 12, color: Color(0xFF8B949E)),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildFixCommandSection(FixSuggestion fix) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF0D1117),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: const Color(0xFF388BFD).withAlpha(50)),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
const Icon(Icons.build_circle_outlined, size: 15, color: Color(0xFF3FB950)),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
fix.title,
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
color: Color(0xFF3FB950),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF161B22),
|
|
borderRadius: BorderRadius.circular(6),
|
|
border: Border.all(color: const Color(0xFF30363D)),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: SelectableText(
|
|
fix.command,
|
|
style: const TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 12,
|
|
color: Color(0xFF79C0FF),
|
|
),
|
|
),
|
|
),
|
|
IconButton(
|
|
tooltip: 'Скопировать команду решения',
|
|
icon: const Icon(Icons.copy_rounded, size: 16, color: Color(0xFF58A6FF)),
|
|
onPressed: () {
|
|
Clipboard.setData(ClipboardData(text: fix.command));
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Команда скопирована: ${fix.command}'),
|
|
duration: const Duration(seconds: 2),
|
|
backgroundColor: const Color(0xFF238636),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (fix.explanation.isNotEmpty) ...[
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
fix.explanation,
|
|
style: const TextStyle(fontSize: 11, color: Color(0xFF8B949E)),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showRawLogModal(BuildContext context, String title, String rawLog) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) {
|
|
return Dialog(
|
|
backgroundColor: const Color(0xFF161B22),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
side: const BorderSide(color: Color(0xFF30363D)),
|
|
),
|
|
child: Container(
|
|
constraints: const BoxConstraints(maxWidth: 750, maxHeight: 600),
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
'Системный лог: $title',
|
|
style: const TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.bold,
|
|
color: Color(0xFFE6EDF3),
|
|
),
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.close_rounded, color: Color(0xFF8B949E)),
|
|
onPressed: () => Navigator.pop(context),
|
|
),
|
|
],
|
|
),
|
|
const Divider(color: Color(0xFF30363D)),
|
|
const SizedBox(height: 8),
|
|
Expanded(
|
|
child: Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF0D1117),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: const Color(0xFF21262D)),
|
|
),
|
|
child: SingleChildScrollView(
|
|
child: SelectableText(
|
|
rawLog.isEmpty ? 'Лог пуст или нет дополнительных записей' : rawLog,
|
|
style: const TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 12,
|
|
color: Color(0xFFC9D1D9),
|
|
height: 1.4,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
TextButton.icon(
|
|
onPressed: () {
|
|
Clipboard.setData(ClipboardData(text: rawLog));
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Лог скопирован в буфер обмена'),
|
|
duration: Duration(seconds: 2),
|
|
),
|
|
);
|
|
},
|
|
icon: const Icon(Icons.copy_rounded, size: 16),
|
|
label: const Text('Скопировать весь лог'),
|
|
),
|
|
const SizedBox(width: 8),
|
|
ElevatedButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: const Text('Закрыть'),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
void _showPromptModal(BuildContext context, String promptPreview) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) {
|
|
return Dialog(
|
|
backgroundColor: const Color(0xFF161B22),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
side: const BorderSide(color: Color(0xFF30363D)),
|
|
),
|
|
child: Container(
|
|
constraints: const BoxConstraints(maxWidth: 700, maxHeight: 600),
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Text(
|
|
'Структурированный JSON-промпт для LLM',
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.bold,
|
|
color: Color(0xFFE6EDF3),
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.close_rounded, color: Color(0xFF8B949E)),
|
|
onPressed: () => Navigator.pop(context),
|
|
),
|
|
],
|
|
),
|
|
const Divider(color: Color(0xFF30363D)),
|
|
const SizedBox(height: 8),
|
|
Expanded(
|
|
child: Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF0D1117),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: SingleChildScrollView(
|
|
child: SelectableText(
|
|
promptPreview,
|
|
style: const TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 11,
|
|
color: Color(0xFF79C0FF),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Align(
|
|
alignment: Alignment.centerRight,
|
|
child: ElevatedButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: const Text('Закрыть'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|