feat: initial commit of diagd - unified Linux/BSD/macOS/Windows diagnostic tool (TUI + GUI)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/target
|
||||
Generated
+2090
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
[package]
|
||||
name = "rust_lib_diagd"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "rust_lib_diagd"
|
||||
crate-type = ["cdylib", "staticlib", "rlib"]
|
||||
|
||||
[[bin]]
|
||||
name = "diagd"
|
||||
path = "src/bin/diagd.rs"
|
||||
required-features = ["tui"]
|
||||
|
||||
[features]
|
||||
default = ["tui", "gui"]
|
||||
tui = ["dep:ratatui", "dep:crossterm"]
|
||||
gui = ["dep:flutter_rust_bridge"]
|
||||
|
||||
[dependencies]
|
||||
diagd-core = { path = "../crates/core" }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
regex = "1.10"
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
|
||||
# GUI-specific
|
||||
flutter_rust_bridge = { version = "=2.13.0", optional = true }
|
||||
|
||||
# TUI-specific
|
||||
ratatui = { version = "0.29", optional = true }
|
||||
crossterm = { version = "0.28", optional = true }
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(frb_expand)'] }
|
||||
@@ -0,0 +1,846 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Уровень критичности обнаруженной проблемы
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum Severity {
|
||||
Critical,
|
||||
Warning,
|
||||
Info,
|
||||
}
|
||||
|
||||
/// Исследуемая подсистема ОС Linux / BSD
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum Subsystem {
|
||||
Systemd,
|
||||
Kernel,
|
||||
Storage,
|
||||
Audio,
|
||||
Network,
|
||||
Security,
|
||||
Crashes,
|
||||
}
|
||||
|
||||
/// Статус состояния подсистемы для сводного дашборда
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SubsystemStatus {
|
||||
pub name: Subsystem,
|
||||
pub status: String,
|
||||
pub issue_count: u32,
|
||||
pub details: String,
|
||||
}
|
||||
|
||||
/// Предложение по устранению ошибки с готовой CLI-командой
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FixSuggestion {
|
||||
pub title: String,
|
||||
pub command: String,
|
||||
pub explanation: String,
|
||||
}
|
||||
|
||||
/// Информация о сбойной системной службе
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FailedService {
|
||||
pub name: String,
|
||||
pub init_system: String,
|
||||
pub state: String,
|
||||
pub description: String,
|
||||
pub logs: String,
|
||||
pub restart_command: String,
|
||||
pub status_command: String,
|
||||
}
|
||||
|
||||
/// Элемент диагностического отчета (проблема)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct IssueItem {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub subsystem: Subsystem,
|
||||
pub severity: Severity,
|
||||
pub root_cause: String,
|
||||
pub explanation: String,
|
||||
pub fix: Option<FixSuggestion>,
|
||||
pub raw_log: String,
|
||||
pub sanitized: bool,
|
||||
pub permission_denied: bool,
|
||||
}
|
||||
|
||||
/// Подробный отчет об аппаратном обеспечении системы
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HardwareInfo {
|
||||
pub cpu: CpuInfo,
|
||||
pub memory: MemoryInfo,
|
||||
pub motherboard: MotherboardInfo,
|
||||
pub gpus: Vec<GpuInfo>,
|
||||
pub disks: Vec<DiskInfo>,
|
||||
pub network_adapters: Vec<NetworkAdapterInfo>,
|
||||
pub battery: Option<BatteryInfo>,
|
||||
pub thermals: ThermalsInfo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CpuInfo {
|
||||
pub model_name: String,
|
||||
pub architecture: String,
|
||||
pub physical_cores: usize,
|
||||
pub logical_threads: usize,
|
||||
pub base_frequency_mhz: Option<f32>,
|
||||
pub current_frequency_mhz: Option<f32>,
|
||||
pub l1_cache: String,
|
||||
pub l2_cache: String,
|
||||
pub l3_cache: String,
|
||||
pub virtualization: Option<String>,
|
||||
pub flags: Vec<String>,
|
||||
pub microcode: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MemoryInfo {
|
||||
pub total_bytes: u64,
|
||||
pub available_bytes: u64,
|
||||
pub used_bytes: u64,
|
||||
pub swap_total_bytes: u64,
|
||||
pub swap_used_bytes: u64,
|
||||
pub edac_ce_count: u64,
|
||||
pub edac_ue_count: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MotherboardInfo {
|
||||
pub vendor: String,
|
||||
pub product_name: String,
|
||||
pub version: String,
|
||||
pub bios_vendor: String,
|
||||
pub bios_version: String,
|
||||
pub bios_date: String,
|
||||
pub chassis_type: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GpuInfo {
|
||||
pub vendor: String,
|
||||
pub model: String,
|
||||
pub driver: String,
|
||||
pub pci_slot: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PartitionInfo {
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
pub mount_point: Option<String>,
|
||||
pub fs_type: String,
|
||||
pub size_bytes: u64,
|
||||
pub used_bytes: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DiskInfo {
|
||||
pub name: String,
|
||||
pub model: String,
|
||||
pub serial: String,
|
||||
pub size_bytes: u64,
|
||||
pub disk_type: String,
|
||||
pub rotational_speed_rpm: Option<u32>,
|
||||
pub smart_status: String,
|
||||
pub health_percent: Option<u8>,
|
||||
pub wear_percent: Option<u8>,
|
||||
pub nvme_wear_percent: Option<u8>,
|
||||
pub temperature_c: Option<i32>,
|
||||
pub power_on_hours: Option<u64>,
|
||||
pub power_cycle_count: Option<u64>,
|
||||
pub bad_sectors_count: Option<u64>,
|
||||
pub total_written_tb: Option<f64>,
|
||||
pub partitions: Vec<PartitionInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NetworkAdapterInfo {
|
||||
pub interface_name: String,
|
||||
pub if_type: String,
|
||||
pub state: String,
|
||||
pub speed_mbps: Option<u32>,
|
||||
pub mac_address: String,
|
||||
pub driver: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BatteryInfo {
|
||||
pub name: String,
|
||||
pub capacity_percent: u8,
|
||||
pub health_percent: f32,
|
||||
pub cycle_count: u32,
|
||||
pub status: String,
|
||||
pub is_degraded: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ThermalsInfo {
|
||||
pub max_cpu_temp_c: f32,
|
||||
pub thermal_throttled_cores: usize,
|
||||
pub total_throttle_events: u64,
|
||||
pub is_actively_throttling: bool,
|
||||
}
|
||||
|
||||
// --- LIVE METRICS ---
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub struct LiveMetricsSnapshot {
|
||||
pub cpu_usage_percent: f32,
|
||||
pub ram_used_bytes: u64,
|
||||
pub ram_total_bytes: u64,
|
||||
pub swap_used_bytes: u64,
|
||||
pub swap_total_bytes: u64,
|
||||
pub max_cpu_temp_c: f32,
|
||||
pub is_throttling: bool,
|
||||
pub disk_read_kbps: u64,
|
||||
pub disk_write_kbps: u64,
|
||||
pub net_rx_kbps: u64,
|
||||
pub net_tx_kbps: u64,
|
||||
}
|
||||
|
||||
// --- CRASH DUMPS ---
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub struct CrashReportInfo {
|
||||
pub timestamp: String,
|
||||
pub process_name: String,
|
||||
pub pid: Option<u32>,
|
||||
pub signal_or_code: String,
|
||||
pub fault_module: Option<String>,
|
||||
pub stack_trace_snippet: String,
|
||||
pub dump_path: Option<String>,
|
||||
}
|
||||
|
||||
// --- SECURITY AUDIT ---
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ListeningPortInfo {
|
||||
pub protocol: String,
|
||||
pub local_address: String,
|
||||
pub port: u16,
|
||||
pub process_name: String,
|
||||
pub pid: Option<u32>,
|
||||
pub is_public: bool,
|
||||
pub risk_level: Severity,
|
||||
pub recommendation: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub struct SuidAnomalyInfo {
|
||||
pub path: String,
|
||||
pub owner: String,
|
||||
pub permissions: String,
|
||||
pub risk_reason: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub struct SecurityCheckItem {
|
||||
pub category: String,
|
||||
pub name: String,
|
||||
pub status: String,
|
||||
pub is_secure: bool,
|
||||
pub description: String,
|
||||
pub remediation: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub struct SecurityAuditReport {
|
||||
pub exposed_ports: Vec<ListeningPortInfo>,
|
||||
pub suid_anomalies: Vec<SuidAnomalyInfo>,
|
||||
pub hardening_checks: Vec<SecurityCheckItem>,
|
||||
pub security_score: u8,
|
||||
}
|
||||
|
||||
/// Общий итоговый отчет диагностики
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SystemReport {
|
||||
pub timestamp: String,
|
||||
pub os_family: String,
|
||||
pub init_system: String,
|
||||
pub is_root: bool,
|
||||
pub subsystems: Vec<SubsystemStatus>,
|
||||
pub issues: Vec<IssueItem>,
|
||||
pub failed_services: Vec<FailedService>,
|
||||
pub hardware: HardwareInfo,
|
||||
pub crashes: Vec<CrashReportInfo>,
|
||||
pub security: SecurityAuditReport,
|
||||
pub llm_analyzed: bool,
|
||||
pub summary: String,
|
||||
pub prompt_preview: String,
|
||||
}
|
||||
|
||||
// Конвертеры из diagd-core в FRB структуры
|
||||
impl From<diagd_core::models::Severity> for Severity {
|
||||
fn from(s: diagd_core::models::Severity) -> Self {
|
||||
match s {
|
||||
diagd_core::models::Severity::Critical => Severity::Critical,
|
||||
diagd_core::models::Severity::Warning => Severity::Warning,
|
||||
diagd_core::models::Severity::Info => Severity::Info,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Severity> for diagd_core::models::Severity {
|
||||
fn from(s: Severity) -> Self {
|
||||
match s {
|
||||
Severity::Critical => diagd_core::models::Severity::Critical,
|
||||
Severity::Warning => diagd_core::models::Severity::Warning,
|
||||
Severity::Info => diagd_core::models::Severity::Info,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<diagd_core::models::Subsystem> for Subsystem {
|
||||
fn from(s: diagd_core::models::Subsystem) -> Self {
|
||||
match s {
|
||||
diagd_core::models::Subsystem::Systemd => Subsystem::Systemd,
|
||||
diagd_core::models::Subsystem::Kernel => Subsystem::Kernel,
|
||||
diagd_core::models::Subsystem::Storage => Subsystem::Storage,
|
||||
diagd_core::models::Subsystem::Audio => Subsystem::Audio,
|
||||
diagd_core::models::Subsystem::Network => Subsystem::Network,
|
||||
diagd_core::models::Subsystem::Security => Subsystem::Security,
|
||||
diagd_core::models::Subsystem::Crashes => Subsystem::Crashes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<diagd_core::models::SubsystemStatus> for SubsystemStatus {
|
||||
fn from(s: diagd_core::models::SubsystemStatus) -> Self {
|
||||
SubsystemStatus {
|
||||
name: s.name.into(),
|
||||
status: s.status,
|
||||
issue_count: s.issue_count,
|
||||
details: s.details,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<diagd_core::models::FixSuggestion> for FixSuggestion {
|
||||
fn from(f: diagd_core::models::FixSuggestion) -> Self {
|
||||
FixSuggestion {
|
||||
title: f.title,
|
||||
command: f.command,
|
||||
explanation: f.explanation,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<diagd_core::models::FailedService> for FailedService {
|
||||
fn from(f: diagd_core::models::FailedService) -> Self {
|
||||
FailedService {
|
||||
name: f.name,
|
||||
init_system: f.init_system,
|
||||
state: f.state,
|
||||
description: f.description,
|
||||
logs: f.logs,
|
||||
restart_command: f.restart_command,
|
||||
status_command: f.status_command,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<diagd_core::models::IssueItem> for IssueItem {
|
||||
fn from(i: diagd_core::models::IssueItem) -> Self {
|
||||
IssueItem {
|
||||
id: i.id,
|
||||
title: i.title,
|
||||
subsystem: i.subsystem.into(),
|
||||
severity: i.severity.into(),
|
||||
root_cause: i.root_cause,
|
||||
explanation: i.explanation,
|
||||
fix: i.fix.map(Into::into),
|
||||
raw_log: i.raw_log,
|
||||
sanitized: i.sanitized,
|
||||
permission_denied: i.permission_denied,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<diagd_core::models::CpuInfo> for CpuInfo {
|
||||
fn from(c: diagd_core::models::CpuInfo) -> Self {
|
||||
Self {
|
||||
model_name: c.model_name,
|
||||
architecture: c.architecture,
|
||||
physical_cores: c.physical_cores,
|
||||
logical_threads: c.logical_threads,
|
||||
base_frequency_mhz: c.base_frequency_mhz,
|
||||
current_frequency_mhz: c.current_frequency_mhz,
|
||||
l1_cache: c.l1_cache,
|
||||
l2_cache: c.l2_cache,
|
||||
l3_cache: c.l3_cache,
|
||||
virtualization: c.virtualization,
|
||||
flags: c.flags,
|
||||
microcode: c.microcode,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<diagd_core::models::MemoryInfo> for MemoryInfo {
|
||||
fn from(m: diagd_core::models::MemoryInfo) -> Self {
|
||||
Self {
|
||||
total_bytes: m.total_bytes,
|
||||
available_bytes: m.available_bytes,
|
||||
used_bytes: m.used_bytes,
|
||||
swap_total_bytes: m.swap_total_bytes,
|
||||
swap_used_bytes: m.swap_used_bytes,
|
||||
edac_ce_count: m.edac_ce_count,
|
||||
edac_ue_count: m.edac_ue_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<diagd_core::models::MotherboardInfo> for MotherboardInfo {
|
||||
fn from(m: diagd_core::models::MotherboardInfo) -> Self {
|
||||
Self {
|
||||
vendor: m.vendor,
|
||||
product_name: m.product_name,
|
||||
version: m.version,
|
||||
bios_vendor: m.bios_vendor,
|
||||
bios_version: m.bios_version,
|
||||
bios_date: m.bios_date,
|
||||
chassis_type: m.chassis_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<diagd_core::models::GpuInfo> for GpuInfo {
|
||||
fn from(g: diagd_core::models::GpuInfo) -> Self {
|
||||
Self {
|
||||
vendor: g.vendor,
|
||||
model: g.model,
|
||||
driver: g.driver,
|
||||
pci_slot: g.pci_slot,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<diagd_core::models::PartitionInfo> for PartitionInfo {
|
||||
fn from(p: diagd_core::models::PartitionInfo) -> Self {
|
||||
Self {
|
||||
name: p.name,
|
||||
path: p.path,
|
||||
mount_point: p.mount_point,
|
||||
fs_type: p.fs_type,
|
||||
size_bytes: p.size_bytes,
|
||||
used_bytes: p.used_bytes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<diagd_core::models::DiskInfo> for DiskInfo {
|
||||
fn from(d: diagd_core::models::DiskInfo) -> Self {
|
||||
Self {
|
||||
name: d.name,
|
||||
model: d.model,
|
||||
serial: d.serial,
|
||||
size_bytes: d.size_bytes,
|
||||
disk_type: d.disk_type,
|
||||
rotational_speed_rpm: d.rotational_speed_rpm,
|
||||
smart_status: d.smart_status,
|
||||
health_percent: d.health_percent,
|
||||
wear_percent: d.wear_percent,
|
||||
nvme_wear_percent: d.nvme_wear_percent,
|
||||
temperature_c: d.temperature_c,
|
||||
power_on_hours: d.power_on_hours,
|
||||
power_cycle_count: d.power_cycle_count,
|
||||
bad_sectors_count: d.bad_sectors_count,
|
||||
total_written_tb: d.total_written_tb,
|
||||
partitions: d.partitions.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<diagd_core::models::NetworkAdapterInfo> for NetworkAdapterInfo {
|
||||
fn from(n: diagd_core::models::NetworkAdapterInfo) -> Self {
|
||||
Self {
|
||||
interface_name: n.interface_name,
|
||||
if_type: n.if_type,
|
||||
state: n.state,
|
||||
speed_mbps: n.speed_mbps,
|
||||
mac_address: n.mac_address,
|
||||
driver: n.driver,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<diagd_core::models::BatteryInfo> for BatteryInfo {
|
||||
fn from(b: diagd_core::models::BatteryInfo) -> Self {
|
||||
Self {
|
||||
name: b.name,
|
||||
capacity_percent: b.capacity_percent,
|
||||
health_percent: b.health_percent,
|
||||
cycle_count: b.cycle_count,
|
||||
status: b.status,
|
||||
is_degraded: b.is_degraded,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<diagd_core::models::ThermalsInfo> for ThermalsInfo {
|
||||
fn from(t: diagd_core::models::ThermalsInfo) -> Self {
|
||||
Self {
|
||||
max_cpu_temp_c: t.max_cpu_temp_c,
|
||||
thermal_throttled_cores: t.thermal_throttled_cores,
|
||||
total_throttle_events: t.total_throttle_events,
|
||||
is_actively_throttling: t.is_actively_throttling,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<diagd_core::models::HardwareInfo> for HardwareInfo {
|
||||
fn from(h: diagd_core::models::HardwareInfo) -> Self {
|
||||
Self {
|
||||
cpu: h.cpu.into(),
|
||||
memory: h.memory.into(),
|
||||
motherboard: h.motherboard.into(),
|
||||
gpus: h.gpus.into_iter().map(Into::into).collect(),
|
||||
disks: h.disks.into_iter().map(Into::into).collect(),
|
||||
network_adapters: h.network_adapters.into_iter().map(Into::into).collect(),
|
||||
battery: h.battery.map(Into::into),
|
||||
thermals: h.thermals.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HardwareInfo> for diagd_core::models::HardwareInfo {
|
||||
fn from(h: HardwareInfo) -> Self {
|
||||
diagd_core::models::HardwareInfo {
|
||||
cpu: diagd_core::models::CpuInfo {
|
||||
model_name: h.cpu.model_name,
|
||||
architecture: h.cpu.architecture,
|
||||
physical_cores: h.cpu.physical_cores,
|
||||
logical_threads: h.cpu.logical_threads,
|
||||
base_frequency_mhz: h.cpu.base_frequency_mhz,
|
||||
current_frequency_mhz: h.cpu.current_frequency_mhz,
|
||||
l1_cache: h.cpu.l1_cache,
|
||||
l2_cache: h.cpu.l2_cache,
|
||||
l3_cache: h.cpu.l3_cache,
|
||||
virtualization: h.cpu.virtualization,
|
||||
flags: h.cpu.flags,
|
||||
microcode: h.cpu.microcode,
|
||||
},
|
||||
memory: diagd_core::models::MemoryInfo {
|
||||
total_bytes: h.memory.total_bytes,
|
||||
available_bytes: h.memory.available_bytes,
|
||||
used_bytes: h.memory.used_bytes,
|
||||
swap_total_bytes: h.memory.swap_total_bytes,
|
||||
swap_used_bytes: h.memory.swap_used_bytes,
|
||||
edac_ce_count: h.memory.edac_ce_count,
|
||||
edac_ue_count: h.memory.edac_ue_count,
|
||||
},
|
||||
motherboard: diagd_core::models::MotherboardInfo {
|
||||
vendor: h.motherboard.vendor,
|
||||
product_name: h.motherboard.product_name,
|
||||
version: h.motherboard.version,
|
||||
bios_vendor: h.motherboard.bios_vendor,
|
||||
bios_version: h.motherboard.bios_version,
|
||||
bios_date: h.motherboard.bios_date,
|
||||
chassis_type: h.motherboard.chassis_type,
|
||||
},
|
||||
gpus: h.gpus.into_iter().map(|g| diagd_core::models::GpuInfo {
|
||||
vendor: g.vendor,
|
||||
model: g.model,
|
||||
driver: g.driver,
|
||||
pci_slot: g.pci_slot,
|
||||
}).collect(),
|
||||
disks: h.disks.into_iter().map(|d| diagd_core::models::DiskInfo {
|
||||
name: d.name,
|
||||
model: d.model,
|
||||
serial: d.serial,
|
||||
size_bytes: d.size_bytes,
|
||||
disk_type: d.disk_type,
|
||||
rotational_speed_rpm: d.rotational_speed_rpm,
|
||||
smart_status: d.smart_status,
|
||||
health_percent: d.health_percent,
|
||||
wear_percent: d.wear_percent,
|
||||
nvme_wear_percent: d.nvme_wear_percent,
|
||||
temperature_c: d.temperature_c,
|
||||
power_on_hours: d.power_on_hours,
|
||||
power_cycle_count: d.power_cycle_count,
|
||||
bad_sectors_count: d.bad_sectors_count,
|
||||
total_written_tb: d.total_written_tb,
|
||||
partitions: d.partitions.into_iter().map(|p| diagd_core::models::PartitionInfo {
|
||||
name: p.name,
|
||||
path: p.path,
|
||||
mount_point: p.mount_point,
|
||||
fs_type: p.fs_type,
|
||||
size_bytes: p.size_bytes,
|
||||
used_bytes: p.used_bytes,
|
||||
}).collect(),
|
||||
}).collect(),
|
||||
network_adapters: h.network_adapters.into_iter().map(|n| diagd_core::models::NetworkAdapterInfo {
|
||||
interface_name: n.interface_name,
|
||||
if_type: n.if_type,
|
||||
state: n.state,
|
||||
speed_mbps: n.speed_mbps,
|
||||
mac_address: n.mac_address,
|
||||
driver: n.driver,
|
||||
}).collect(),
|
||||
battery: h.battery.map(|b| diagd_core::models::BatteryInfo {
|
||||
name: b.name,
|
||||
capacity_percent: b.capacity_percent,
|
||||
health_percent: b.health_percent,
|
||||
cycle_count: b.cycle_count,
|
||||
status: b.status,
|
||||
is_degraded: b.is_degraded,
|
||||
}),
|
||||
thermals: diagd_core::models::ThermalsInfo {
|
||||
max_cpu_temp_c: h.thermals.max_cpu_temp_c,
|
||||
thermal_throttled_cores: h.thermals.thermal_throttled_cores,
|
||||
total_throttle_events: h.thermals.total_throttle_events,
|
||||
is_actively_throttling: h.thermals.is_actively_throttling,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<diagd_core::models::LiveMetricsSnapshot> for LiveMetricsSnapshot {
|
||||
fn from(s: diagd_core::models::LiveMetricsSnapshot) -> Self {
|
||||
LiveMetricsSnapshot {
|
||||
cpu_usage_percent: s.cpu_usage_percent,
|
||||
ram_used_bytes: s.ram_used_bytes,
|
||||
ram_total_bytes: s.ram_total_bytes,
|
||||
swap_used_bytes: s.swap_used_bytes,
|
||||
swap_total_bytes: s.swap_total_bytes,
|
||||
max_cpu_temp_c: s.max_cpu_temp_c,
|
||||
is_throttling: s.is_throttling,
|
||||
disk_read_kbps: s.disk_read_kbps,
|
||||
disk_write_kbps: s.disk_write_kbps,
|
||||
net_rx_kbps: s.net_rx_kbps,
|
||||
net_tx_kbps: s.net_tx_kbps,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<diagd_core::models::CrashReportInfo> for CrashReportInfo {
|
||||
fn from(c: diagd_core::models::CrashReportInfo) -> Self {
|
||||
CrashReportInfo {
|
||||
timestamp: c.timestamp,
|
||||
process_name: c.process_name,
|
||||
pid: c.pid,
|
||||
signal_or_code: c.signal_or_code,
|
||||
fault_module: c.fault_module,
|
||||
stack_trace_snippet: c.stack_trace_snippet,
|
||||
dump_path: c.dump_path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CrashReportInfo> for diagd_core::models::CrashReportInfo {
|
||||
fn from(c: CrashReportInfo) -> Self {
|
||||
diagd_core::models::CrashReportInfo {
|
||||
timestamp: c.timestamp,
|
||||
process_name: c.process_name,
|
||||
pid: c.pid,
|
||||
signal_or_code: c.signal_or_code,
|
||||
fault_module: c.fault_module,
|
||||
stack_trace_snippet: c.stack_trace_snippet,
|
||||
dump_path: c.dump_path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<diagd_core::models::ListeningPortInfo> for ListeningPortInfo {
|
||||
fn from(p: diagd_core::models::ListeningPortInfo) -> Self {
|
||||
ListeningPortInfo {
|
||||
protocol: p.protocol,
|
||||
local_address: p.local_address,
|
||||
port: p.port,
|
||||
process_name: p.process_name,
|
||||
pid: p.pid,
|
||||
is_public: p.is_public,
|
||||
risk_level: p.risk_level.into(),
|
||||
recommendation: p.recommendation,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ListeningPortInfo> for diagd_core::models::ListeningPortInfo {
|
||||
fn from(p: ListeningPortInfo) -> Self {
|
||||
diagd_core::models::ListeningPortInfo {
|
||||
protocol: p.protocol,
|
||||
local_address: p.local_address,
|
||||
port: p.port,
|
||||
process_name: p.process_name,
|
||||
pid: p.pid,
|
||||
is_public: p.is_public,
|
||||
risk_level: p.risk_level.into(),
|
||||
recommendation: p.recommendation,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<diagd_core::models::SuidAnomalyInfo> for SuidAnomalyInfo {
|
||||
fn from(s: diagd_core::models::SuidAnomalyInfo) -> Self {
|
||||
SuidAnomalyInfo {
|
||||
path: s.path,
|
||||
owner: s.owner,
|
||||
permissions: s.permissions,
|
||||
risk_reason: s.risk_reason,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SuidAnomalyInfo> for diagd_core::models::SuidAnomalyInfo {
|
||||
fn from(s: SuidAnomalyInfo) -> Self {
|
||||
diagd_core::models::SuidAnomalyInfo {
|
||||
path: s.path,
|
||||
owner: s.owner,
|
||||
permissions: s.permissions,
|
||||
risk_reason: s.risk_reason,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<diagd_core::models::SecurityCheckItem> for SecurityCheckItem {
|
||||
fn from(c: diagd_core::models::SecurityCheckItem) -> Self {
|
||||
SecurityCheckItem {
|
||||
category: c.category,
|
||||
name: c.name,
|
||||
status: c.status,
|
||||
is_secure: c.is_secure,
|
||||
description: c.description,
|
||||
remediation: c.remediation,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SecurityCheckItem> for diagd_core::models::SecurityCheckItem {
|
||||
fn from(c: SecurityCheckItem) -> Self {
|
||||
diagd_core::models::SecurityCheckItem {
|
||||
category: c.category,
|
||||
name: c.name,
|
||||
status: c.status,
|
||||
is_secure: c.is_secure,
|
||||
description: c.description,
|
||||
remediation: c.remediation,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<diagd_core::models::SecurityAuditReport> for SecurityAuditReport {
|
||||
fn from(s: diagd_core::models::SecurityAuditReport) -> Self {
|
||||
SecurityAuditReport {
|
||||
exposed_ports: s.exposed_ports.into_iter().map(Into::into).collect(),
|
||||
suid_anomalies: s.suid_anomalies.into_iter().map(Into::into).collect(),
|
||||
hardening_checks: s.hardening_checks.into_iter().map(Into::into).collect(),
|
||||
security_score: s.security_score,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SecurityAuditReport> for diagd_core::models::SecurityAuditReport {
|
||||
fn from(s: SecurityAuditReport) -> Self {
|
||||
diagd_core::models::SecurityAuditReport {
|
||||
exposed_ports: s.exposed_ports.into_iter().map(Into::into).collect(),
|
||||
suid_anomalies: s.suid_anomalies.into_iter().map(Into::into).collect(),
|
||||
hardening_checks: s.hardening_checks.into_iter().map(Into::into).collect(),
|
||||
security_score: s.security_score,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<diagd_core::models::SystemReport> for SystemReport {
|
||||
fn from(r: diagd_core::models::SystemReport) -> Self {
|
||||
SystemReport {
|
||||
timestamp: r.timestamp,
|
||||
os_family: r.os_family,
|
||||
init_system: r.init_system,
|
||||
is_root: r.is_root,
|
||||
subsystems: r.subsystems.into_iter().map(Into::into).collect(),
|
||||
issues: r.issues.into_iter().map(Into::into).collect(),
|
||||
failed_services: r.failed_services.into_iter().map(Into::into).collect(),
|
||||
hardware: r.hardware.into(),
|
||||
crashes: r.crashes.into_iter().map(Into::into).collect(),
|
||||
security: r.security.into(),
|
||||
llm_analyzed: r.llm_analyzed,
|
||||
summary: r.summary,
|
||||
prompt_preview: r.prompt_preview,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Запрашивает права суперпользователя в графическом режиме через Polkit (pkexec)
|
||||
pub fn request_gui_privileges() -> bool {
|
||||
let status = diagd_core::auth::PrivilegeManager::request_privileges(diagd_core::auth::AuthMode::Gui);
|
||||
matches!(status, diagd_core::auth::PrivilegeStatus::AlreadyRoot | diagd_core::auth::PrivilegeStatus::Elevated)
|
||||
}
|
||||
|
||||
/// Сканирует состояние системы, очищает логи от персональных данных,
|
||||
/// генерирует структурированный промпт для LLM и формирует итоговый аналитический отчет.
|
||||
pub async fn scan_system_and_analyze() -> Result<SystemReport, String> {
|
||||
let core_report = diagd_core::analyst::run_scan_with_mode(diagd_core::auth::AuthMode::Gui).await?;
|
||||
Ok(core_report.into())
|
||||
}
|
||||
|
||||
/// Возвращает мгновенный снимок телеметрии системы в реальном времени (CPU %, RAM, частоты, температуры, IO)
|
||||
pub fn get_live_metrics() -> LiveMetricsSnapshot {
|
||||
diagd_core::LiveMetricsScanner::sample().into()
|
||||
}
|
||||
|
||||
/// Экспортирует полный архив Debug Bundle (.tar.gz, standalone HTML и markdown).
|
||||
/// Возвращает путь к сохраненному архиву.
|
||||
pub fn export_debug_bundle(report: SystemReport) -> Result<String, String> {
|
||||
let core_report = diagd_core::models::SystemReport {
|
||||
timestamp: report.timestamp,
|
||||
os_family: report.os_family,
|
||||
init_system: report.init_system,
|
||||
is_root: report.is_root,
|
||||
subsystems: report.subsystems.into_iter().map(|s| diagd_core::models::SubsystemStatus {
|
||||
name: match s.name {
|
||||
Subsystem::Systemd => diagd_core::models::Subsystem::Systemd,
|
||||
Subsystem::Kernel => diagd_core::models::Subsystem::Kernel,
|
||||
Subsystem::Storage => diagd_core::models::Subsystem::Storage,
|
||||
Subsystem::Audio => diagd_core::models::Subsystem::Audio,
|
||||
Subsystem::Network => diagd_core::models::Subsystem::Network,
|
||||
Subsystem::Security => diagd_core::models::Subsystem::Security,
|
||||
Subsystem::Crashes => diagd_core::models::Subsystem::Crashes,
|
||||
},
|
||||
status: s.status,
|
||||
issue_count: s.issue_count,
|
||||
details: s.details,
|
||||
}).collect(),
|
||||
issues: report.issues.into_iter().map(|i| diagd_core::models::IssueItem {
|
||||
id: i.id,
|
||||
title: i.title,
|
||||
subsystem: match i.subsystem {
|
||||
Subsystem::Systemd => diagd_core::models::Subsystem::Systemd,
|
||||
Subsystem::Kernel => diagd_core::models::Subsystem::Kernel,
|
||||
Subsystem::Storage => diagd_core::models::Subsystem::Storage,
|
||||
Subsystem::Audio => diagd_core::models::Subsystem::Audio,
|
||||
Subsystem::Network => diagd_core::models::Subsystem::Network,
|
||||
Subsystem::Security => diagd_core::models::Subsystem::Security,
|
||||
Subsystem::Crashes => diagd_core::models::Subsystem::Crashes,
|
||||
},
|
||||
severity: match i.severity {
|
||||
Severity::Critical => diagd_core::models::Severity::Critical,
|
||||
Severity::Warning => diagd_core::models::Severity::Warning,
|
||||
Severity::Info => diagd_core::models::Severity::Info,
|
||||
},
|
||||
root_cause: i.root_cause,
|
||||
explanation: i.explanation,
|
||||
fix: i.fix.map(|f| diagd_core::models::FixSuggestion {
|
||||
title: f.title,
|
||||
command: f.command,
|
||||
explanation: f.explanation,
|
||||
}),
|
||||
raw_log: i.raw_log,
|
||||
sanitized: i.sanitized,
|
||||
permission_denied: i.permission_denied,
|
||||
}).collect(),
|
||||
failed_services: report.failed_services.into_iter().map(|f| diagd_core::models::FailedService {
|
||||
name: f.name,
|
||||
init_system: f.init_system,
|
||||
state: f.state,
|
||||
description: f.description,
|
||||
logs: f.logs,
|
||||
restart_command: f.restart_command,
|
||||
status_command: f.status_command,
|
||||
}).collect(),
|
||||
hardware: report.hardware.into(),
|
||||
crashes: report.crashes.into_iter().map(Into::into).collect(),
|
||||
security: report.security.into(),
|
||||
llm_analyzed: report.llm_analyzed,
|
||||
summary: report.summary,
|
||||
prompt_preview: report.prompt_preview,
|
||||
};
|
||||
|
||||
diagd_core::BundleExporter::export_bundle(&core_report)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod simple;
|
||||
pub mod diagnostics;
|
||||
@@ -0,0 +1,10 @@
|
||||
#[flutter_rust_bridge::frb(sync)] // Synchronous mode for simplicity of the demo
|
||||
pub fn greet(name: String) -> String {
|
||||
format!("Hello, {name}!")
|
||||
}
|
||||
|
||||
#[flutter_rust_bridge::frb(init)]
|
||||
pub fn init_app() {
|
||||
// Default utilities - feel free to customize
|
||||
flutter_rust_bridge::setup_default_user_utils();
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
rust_lib_diagd::tui::run_tui()
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
#[cfg(feature = "gui")]
|
||||
pub mod api;
|
||||
#[cfg(feature = "gui")]
|
||||
mod frb_generated;
|
||||
|
||||
#[cfg(feature = "tui")]
|
||||
pub mod tui;
|
||||
@@ -0,0 +1,399 @@
|
||||
use diagd_core::models::SystemReport;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum ActiveView {
|
||||
Issues,
|
||||
Hardware,
|
||||
LiveMonitor,
|
||||
Security,
|
||||
Crashes,
|
||||
RawLog,
|
||||
LlmPrompt,
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
pub report: Option<SystemReport>,
|
||||
pub selected_issue_idx: usize,
|
||||
pub selected_crash_idx: usize,
|
||||
pub log_scroll: u16,
|
||||
pub active_view: ActiveView,
|
||||
pub status_message: String,
|
||||
pub should_quit: bool,
|
||||
pub live_snapshot: diagd_core::models::LiveMetricsSnapshot,
|
||||
pub cpu_history: Vec<u64>,
|
||||
pub ram_history: Vec<u64>,
|
||||
pub temp_history: Vec<u64>,
|
||||
pub net_history: Vec<u64>,
|
||||
pub disk_history: Vec<u64>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(initial_report: Option<SystemReport>) -> Self {
|
||||
Self {
|
||||
report: initial_report,
|
||||
selected_issue_idx: 0,
|
||||
selected_crash_idx: 0,
|
||||
log_scroll: 0,
|
||||
active_view: ActiveView::Issues,
|
||||
status_message: "Готов к работе. Поддерживается клик и скролл мышкой. [s] Сканировать, [q] Выход.".to_string(),
|
||||
should_quit: false,
|
||||
live_snapshot: diagd_core::models::LiveMetricsSnapshot::default(),
|
||||
cpu_history: Vec::with_capacity(60),
|
||||
ram_history: Vec::with_capacity(60),
|
||||
temp_history: Vec::with_capacity(60),
|
||||
net_history: Vec::with_capacity(60),
|
||||
disk_history: Vec::with_capacity(60),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tick_live_metrics(&mut self) {
|
||||
let snap = diagd_core::LiveMetricsScanner::sample();
|
||||
let cpu_val = snap.cpu_usage_percent.round() as u64;
|
||||
self.cpu_history.push(cpu_val);
|
||||
if self.cpu_history.len() > 60 {
|
||||
self.cpu_history.remove(0);
|
||||
}
|
||||
|
||||
let ram_pct = if snap.ram_total_bytes > 0 {
|
||||
((snap.ram_used_bytes as f64 / snap.ram_total_bytes as f64) * 100.0).round() as u64
|
||||
} else {
|
||||
0
|
||||
};
|
||||
self.ram_history.push(ram_pct);
|
||||
if self.ram_history.len() > 60 {
|
||||
self.ram_history.remove(0);
|
||||
}
|
||||
|
||||
let temp_val = snap.max_cpu_temp_c.max(0.0).round() as u64;
|
||||
self.temp_history.push(temp_val);
|
||||
if self.temp_history.len() > 60 {
|
||||
self.temp_history.remove(0);
|
||||
}
|
||||
|
||||
let net_kb = snap.net_rx_kbps + snap.net_tx_kbps;
|
||||
self.net_history.push(net_kb);
|
||||
if self.net_history.len() > 60 {
|
||||
self.net_history.remove(0);
|
||||
}
|
||||
|
||||
let disk_kb = snap.disk_read_kbps + snap.disk_write_kbps;
|
||||
self.disk_history.push(disk_kb);
|
||||
if self.disk_history.len() > 60 {
|
||||
self.disk_history.remove(0);
|
||||
}
|
||||
|
||||
self.live_snapshot = snap;
|
||||
}
|
||||
|
||||
pub fn next_crash(&mut self) {
|
||||
if let Some(ref report) = self.report {
|
||||
if !report.crashes.is_empty() {
|
||||
if self.selected_crash_idx + 1 < report.crashes.len() {
|
||||
self.selected_crash_idx += 1;
|
||||
} else {
|
||||
self.selected_crash_idx = 0;
|
||||
}
|
||||
self.log_scroll = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn previous_crash(&mut self) {
|
||||
if let Some(ref report) = self.report {
|
||||
if !report.crashes.is_empty() {
|
||||
if self.selected_crash_idx > 0 {
|
||||
self.selected_crash_idx -= 1;
|
||||
} else {
|
||||
self.selected_crash_idx = report.crashes.len() - 1;
|
||||
}
|
||||
self.log_scroll = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_issue(&mut self) {
|
||||
if let Some(ref report) = self.report {
|
||||
if !report.issues.is_empty() {
|
||||
if self.selected_issue_idx + 1 < report.issues.len() {
|
||||
self.selected_issue_idx += 1;
|
||||
} else {
|
||||
self.selected_issue_idx = 0;
|
||||
}
|
||||
self.log_scroll = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn previous_issue(&mut self) {
|
||||
if let Some(ref report) = self.report {
|
||||
if !report.issues.is_empty() {
|
||||
if self.selected_issue_idx > 0 {
|
||||
self.selected_issue_idx -= 1;
|
||||
} else {
|
||||
self.selected_issue_idx = report.issues.len() - 1;
|
||||
}
|
||||
self.log_scroll = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scroll_log_down(&mut self) {
|
||||
self.log_scroll = self.log_scroll.saturating_add(3);
|
||||
}
|
||||
|
||||
pub fn scroll_log_up(&mut self) {
|
||||
self.log_scroll = self.log_scroll.saturating_sub(3);
|
||||
}
|
||||
|
||||
pub fn toggle_raw_log(&mut self) {
|
||||
self.active_view = match self.active_view {
|
||||
ActiveView::RawLog => ActiveView::Issues,
|
||||
_ => {
|
||||
self.log_scroll = 0;
|
||||
ActiveView::RawLog
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toggle_prompt(&mut self) {
|
||||
self.active_view = match self.active_view {
|
||||
ActiveView::LlmPrompt => ActiveView::Issues,
|
||||
_ => {
|
||||
self.log_scroll = 0;
|
||||
ActiveView::LlmPrompt
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toggle_hardware(&mut self) {
|
||||
self.active_view = match self.active_view {
|
||||
ActiveView::Hardware => ActiveView::Issues,
|
||||
_ => {
|
||||
self.log_scroll = 0;
|
||||
ActiveView::Hardware
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toggle_live_monitor(&mut self) {
|
||||
self.active_view = match self.active_view {
|
||||
ActiveView::LiveMonitor => ActiveView::Issues,
|
||||
_ => {
|
||||
self.log_scroll = 0;
|
||||
ActiveView::LiveMonitor
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toggle_security(&mut self) {
|
||||
self.active_view = match self.active_view {
|
||||
ActiveView::Security => ActiveView::Issues,
|
||||
_ => {
|
||||
self.log_scroll = 0;
|
||||
ActiveView::Security
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toggle_crashes(&mut self) {
|
||||
self.active_view = match self.active_view {
|
||||
ActiveView::Crashes => ActiveView::Issues,
|
||||
_ => {
|
||||
self.log_scroll = 0;
|
||||
ActiveView::Crashes
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn copy_fix_command(&mut self) {
|
||||
if let Some(ref report) = self.report {
|
||||
if let Some(issue) = report.issues.get(self.selected_issue_idx) {
|
||||
if let Some(ref fix) = issue.fix {
|
||||
copy_to_clipboard(&fix.command);
|
||||
self.status_message = format!("✓ Скопировано: {}", fix.command);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.status_message = "Нет команды для копирования.".to_string();
|
||||
}
|
||||
|
||||
pub fn export_bundle(&mut self) {
|
||||
if let Some(ref report) = self.report {
|
||||
match diagd_core::BundleExporter::export_bundle(report) {
|
||||
Ok(path) => {
|
||||
self.status_message = format!("✓ Отчет и Debug Bundle сохранены: {}", path);
|
||||
}
|
||||
Err(err) => {
|
||||
self.status_message = format!("Ошибка экспорта: {}", err);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.status_message = "Нет данных для экспорта. Сначала выполните сканирование.".to_string();
|
||||
}
|
||||
}
|
||||
|
||||
/// Интерактивная обработка клика мыши
|
||||
pub fn handle_mouse_click(&mut self, col: u16, row: u16, screen_width: u16, screen_height: u16) {
|
||||
// 0. Клик в верхний Tab Bar (строки 0 или 1)
|
||||
if row <= 2 {
|
||||
if col < 35 {
|
||||
self.active_view = ActiveView::Issues;
|
||||
self.log_scroll = 0;
|
||||
} else if col < 53 {
|
||||
self.active_view = ActiveView::Hardware;
|
||||
self.log_scroll = 0;
|
||||
} else if col < 67 {
|
||||
self.active_view = ActiveView::LiveMonitor;
|
||||
self.log_scroll = 0;
|
||||
} else if col < 84 {
|
||||
self.active_view = ActiveView::Security;
|
||||
self.log_scroll = 0;
|
||||
} else if col < 95 {
|
||||
self.active_view = ActiveView::Crashes;
|
||||
self.log_scroll = 0;
|
||||
} else if col < 105 {
|
||||
self.toggle_raw_log();
|
||||
} else if col < 120 {
|
||||
self.toggle_prompt();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Клик в футер (горячие клавиши)
|
||||
if row >= screen_height.saturating_sub(2) {
|
||||
if col < 12 {
|
||||
self.should_quit = true;
|
||||
} else if col < 26 {
|
||||
// [s] Сканировать
|
||||
self.status_message = "Запрос повторного сканирования...".to_string();
|
||||
} else if col < 40 {
|
||||
// [h] Железо
|
||||
self.toggle_hardware();
|
||||
} else if col < 55 {
|
||||
// Копировать
|
||||
self.copy_fix_command();
|
||||
} else if col < 66 {
|
||||
// Лог
|
||||
self.toggle_raw_log();
|
||||
} else if col < 80 {
|
||||
// Промпт
|
||||
self.toggle_prompt();
|
||||
} else if col < 96 {
|
||||
// Экспорт
|
||||
self.export_bundle();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Клик вне модального окна закрывает его
|
||||
if self.active_view == ActiveView::RawLog || self.active_view == ActiveView::LlmPrompt {
|
||||
if row <= 7 || row >= screen_height.saturating_sub(2) {
|
||||
self.active_view = ActiveView::Issues;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Клик в список инцидентов (левая колонка: 0..width*38%)
|
||||
let list_width = (screen_width as f32 * 0.38) as u16;
|
||||
if self.active_view == ActiveView::Issues && col <= list_width && row >= 8 {
|
||||
let item_index = (row - 8) as usize;
|
||||
if let Some(ref report) = self.report {
|
||||
if item_index < report.issues.len() {
|
||||
self.selected_issue_idx = item_index;
|
||||
self.log_scroll = 0;
|
||||
self.status_message = format!("Выбрана проблема #{}", item_index + 1);
|
||||
}
|
||||
}
|
||||
} else if self.active_view == ActiveView::Crashes && col <= list_width && row >= 8 {
|
||||
let item_index = (row - 8) as usize;
|
||||
if let Some(ref report) = self.report {
|
||||
if item_index < report.crashes.len() {
|
||||
self.selected_crash_idx = item_index;
|
||||
self.log_scroll = 0;
|
||||
self.status_message = format!("Выбран сбой #{}", item_index + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_to_clipboard(text: &str) {
|
||||
use std::io::Write;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
let _ = Command::new("wl-copy")
|
||||
.stdin(Stdio::piped())
|
||||
.spawn()
|
||||
.and_then(|mut child| {
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
stdin.write_all(text.as_bytes())?;
|
||||
}
|
||||
child.wait()
|
||||
})
|
||||
.or_else(|_| {
|
||||
Command::new("xclip")
|
||||
.args(["-selection", "clipboard"])
|
||||
.stdin(Stdio::piped())
|
||||
.spawn()
|
||||
.and_then(|mut child| {
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
stdin.write_all(text.as_bytes())?;
|
||||
}
|
||||
child.wait()
|
||||
})
|
||||
})
|
||||
.or_else(|_| {
|
||||
// macOS pbcopy
|
||||
Command::new("pbcopy")
|
||||
.stdin(Stdio::piped())
|
||||
.spawn()
|
||||
.and_then(|mut child| {
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
stdin.write_all(text.as_bytes())?;
|
||||
}
|
||||
child.wait()
|
||||
})
|
||||
})
|
||||
.or_else(|_| {
|
||||
// Windows clip
|
||||
Command::new("clip")
|
||||
.stdin(Stdio::piped())
|
||||
.spawn()
|
||||
.and_then(|mut child| {
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
stdin.write_all(text.as_bytes())?;
|
||||
}
|
||||
child.wait()
|
||||
})
|
||||
});
|
||||
|
||||
let b64 = base64_simple_encode(text.as_bytes());
|
||||
print!("\x1b]52;c;{}\x07", b64);
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
|
||||
fn base64_simple_encode(input: &[u8]) -> String {
|
||||
const CHARSET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
let mut out = String::new();
|
||||
for chunk in input.chunks(3) {
|
||||
let b0 = chunk[0];
|
||||
let b1 = if chunk.len() > 1 { chunk[1] } else { 0 };
|
||||
let b2 = if chunk.len() > 2 { chunk[2] } else { 0 };
|
||||
|
||||
let n = ((b0 as u32) << 16) | ((b1 as u32) << 8) | (b2 as u32);
|
||||
out.push(CHARSET[((n >> 18) & 63) as usize] as char);
|
||||
out.push(CHARSET[((n >> 12) & 63) as usize] as char);
|
||||
if chunk.len() > 1 {
|
||||
out.push(CHARSET[((n >> 6) & 63) as usize] as char);
|
||||
} else {
|
||||
out.push('=');
|
||||
}
|
||||
if chunk.len() > 2 {
|
||||
out.push(CHARSET[(n & 63) as usize] as char);
|
||||
} else {
|
||||
out.push('=');
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
pub mod app;
|
||||
pub mod ui;
|
||||
|
||||
use app::{ActiveView, App};
|
||||
use crossterm::{
|
||||
event::{
|
||||
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers,
|
||||
MouseButton, MouseEventKind,
|
||||
},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use diagd_core::auth::{AuthMode, PrivilegeManager, PrivilegeStatus};
|
||||
use ratatui::{backend::CrosstermBackend, Terminal};
|
||||
use std::io;
|
||||
use std::time::Duration;
|
||||
|
||||
pub async fn run_tui_async() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 1. Однократная проверка/запрос привилегий перед входом в fullscreen TUI
|
||||
let priv_status = PrivilegeManager::request_privileges(AuthMode::Tui);
|
||||
|
||||
// 2. Инициализация терминала (Raw Mode + Alternate Screen + Mouse Capture)
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
let mut app = App::new(None);
|
||||
|
||||
let auth_msg = match priv_status {
|
||||
PrivilegeStatus::AlreadyRoot => "Запущено от root".to_string(),
|
||||
PrivilegeStatus::Elevated => "Привилегии sudo получены".to_string(),
|
||||
PrivilegeStatus::Unprivileged { reason } => format!("Unprivileged режим: {}", reason),
|
||||
};
|
||||
|
||||
app.status_message = format!("{} | Сканирование системы...", auth_msg);
|
||||
terminal.draw(|f| ui::draw(f, &app))?;
|
||||
|
||||
// 3. Запуск глубокого аудита
|
||||
if let Ok(report) = diagd_core::run_scan().await {
|
||||
app.status_message = format!(
|
||||
"{} ({}) | Найдено проблем: {} | Кликайте мышкой по списку",
|
||||
report.os_family,
|
||||
report.init_system,
|
||||
report.issues.len()
|
||||
);
|
||||
app.report = Some(report);
|
||||
} else {
|
||||
app.status_message = "Ошибка при первичном сканировании системы.".to_string();
|
||||
}
|
||||
|
||||
// 4. Основной цикл событий с поддержкой клавиатуры и мыши
|
||||
let res = run_app(&mut terminal, &mut app).await;
|
||||
|
||||
// 5. Корректное восстановление терминала (отключение мыши, возврат экрана)
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
eprintln!("Ошибка работы TUI: {:?}", err);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run_tui() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let rt = tokio::runtime::Runtime::new()?;
|
||||
rt.block_on(run_tui_async())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn diagd_run_tui() -> i32 {
|
||||
match run_tui() {
|
||||
Ok(_) => 0,
|
||||
Err(e) => {
|
||||
eprintln!("Ошибка запуска TUI: {}", e);
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_app<B: ratatui::backend::Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
app: &mut App,
|
||||
) -> io::Result<()> {
|
||||
let mut last_live_tick = std::time::Instant::now();
|
||||
app.tick_live_metrics();
|
||||
|
||||
loop {
|
||||
terminal.draw(|f| ui::draw(f, app))?;
|
||||
|
||||
if app.should_quit {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if last_live_tick.elapsed() >= Duration::from_millis(1000) {
|
||||
app.tick_live_metrics();
|
||||
last_live_tick = std::time::Instant::now();
|
||||
}
|
||||
|
||||
let size = terminal.size()?;
|
||||
|
||||
if event::poll(Duration::from_millis(100))? {
|
||||
match event::read()? {
|
||||
// Обработка МЫШИ (клик и колесико скролла)
|
||||
Event::Mouse(mouse) => match mouse.kind {
|
||||
MouseEventKind::Down(MouseButton::Left) => {
|
||||
app.handle_mouse_click(mouse.column, mouse.row, size.width, size.height);
|
||||
}
|
||||
MouseEventKind::ScrollDown => {
|
||||
app.scroll_log_down();
|
||||
}
|
||||
MouseEventKind::ScrollUp => {
|
||||
app.scroll_log_up();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
|
||||
// Обработка КЛАВИАТУРЫ
|
||||
Event::Key(key) => {
|
||||
if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Глобальные быстрые переключения вкладок
|
||||
match key.code {
|
||||
KeyCode::Char('1') => {
|
||||
app.active_view = ActiveView::Issues;
|
||||
continue;
|
||||
}
|
||||
KeyCode::Char('2') => {
|
||||
app.active_view = ActiveView::Hardware;
|
||||
continue;
|
||||
}
|
||||
KeyCode::Char('3') => {
|
||||
app.active_view = ActiveView::LiveMonitor;
|
||||
continue;
|
||||
}
|
||||
KeyCode::Char('4') => {
|
||||
app.active_view = ActiveView::Security;
|
||||
continue;
|
||||
}
|
||||
KeyCode::Char('5') => {
|
||||
app.active_view = ActiveView::Crashes;
|
||||
continue;
|
||||
}
|
||||
KeyCode::Char('6') => {
|
||||
app.toggle_raw_log();
|
||||
continue;
|
||||
}
|
||||
KeyCode::Char('7') => {
|
||||
app.toggle_prompt();
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
match app.active_view {
|
||||
ActiveView::Issues => match key.code {
|
||||
KeyCode::Char('q') => {
|
||||
app.should_quit = true;
|
||||
}
|
||||
KeyCode::Char('s') => {
|
||||
app.status_message = "Глубокое сканирование системы...".to_string();
|
||||
terminal.draw(|f| ui::draw(f, app))?;
|
||||
if let Ok(report) = diagd_core::run_scan().await {
|
||||
app.status_message = format!(
|
||||
"{} ({}) | Проблем: {} | Клик мышью для выбора",
|
||||
report.os_family,
|
||||
report.init_system,
|
||||
report.issues.len()
|
||||
);
|
||||
app.report = Some(report);
|
||||
app.selected_issue_idx = 0;
|
||||
} else {
|
||||
app.status_message = "Ошибка при выполнении сканирования!".to_string();
|
||||
}
|
||||
}
|
||||
KeyCode::Down | KeyCode::Char('j') => {
|
||||
app.next_issue();
|
||||
}
|
||||
KeyCode::Up | KeyCode::Char('k') => {
|
||||
app.previous_issue();
|
||||
}
|
||||
KeyCode::Char('c') => {
|
||||
app.copy_fix_command();
|
||||
}
|
||||
KeyCode::Char('h') => {
|
||||
app.toggle_hardware();
|
||||
}
|
||||
KeyCode::Char('m') => {
|
||||
app.toggle_live_monitor();
|
||||
}
|
||||
KeyCode::Char('b') => {
|
||||
app.toggle_security();
|
||||
}
|
||||
KeyCode::Char('x') => {
|
||||
app.toggle_crashes();
|
||||
}
|
||||
KeyCode::Char('l') | KeyCode::Enter => {
|
||||
app.toggle_raw_log();
|
||||
}
|
||||
KeyCode::Char('p') => {
|
||||
app.toggle_prompt();
|
||||
}
|
||||
KeyCode::Char('e') => {
|
||||
app.export_bundle();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
ActiveView::Hardware | ActiveView::Security => match key.code {
|
||||
KeyCode::Esc | KeyCode::Char('q') => {
|
||||
app.active_view = ActiveView::Issues;
|
||||
}
|
||||
KeyCode::Char('e') => {
|
||||
app.export_bundle();
|
||||
}
|
||||
KeyCode::Down | KeyCode::Char('j') => {
|
||||
app.scroll_log_down();
|
||||
}
|
||||
KeyCode::Up | KeyCode::Char('k') => {
|
||||
app.scroll_log_up();
|
||||
}
|
||||
KeyCode::PageDown => {
|
||||
app.log_scroll = app.log_scroll.saturating_add(10);
|
||||
}
|
||||
KeyCode::PageUp => {
|
||||
app.log_scroll = app.log_scroll.saturating_sub(10);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
ActiveView::LiveMonitor => match key.code {
|
||||
KeyCode::Esc | KeyCode::Char('q') => {
|
||||
app.active_view = ActiveView::Issues;
|
||||
}
|
||||
KeyCode::Char('e') => {
|
||||
app.export_bundle();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
ActiveView::Crashes => match key.code {
|
||||
KeyCode::Esc | KeyCode::Char('q') => {
|
||||
app.active_view = ActiveView::Issues;
|
||||
}
|
||||
KeyCode::Down | KeyCode::Char('j') => {
|
||||
app.next_crash();
|
||||
}
|
||||
KeyCode::Up | KeyCode::Char('k') => {
|
||||
app.previous_crash();
|
||||
}
|
||||
KeyCode::PageDown => {
|
||||
app.log_scroll = app.log_scroll.saturating_add(10);
|
||||
}
|
||||
KeyCode::PageUp => {
|
||||
app.log_scroll = app.log_scroll.saturating_sub(10);
|
||||
}
|
||||
KeyCode::Char('e') => {
|
||||
app.export_bundle();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
ActiveView::RawLog | ActiveView::LlmPrompt => match key.code {
|
||||
KeyCode::Esc
|
||||
| KeyCode::Char('q')
|
||||
| KeyCode::Char('l')
|
||||
| KeyCode::Char('p') => {
|
||||
app.active_view = ActiveView::Issues;
|
||||
}
|
||||
KeyCode::Down | KeyCode::Char('j') => {
|
||||
app.scroll_log_down();
|
||||
}
|
||||
KeyCode::Up | KeyCode::Char('k') => {
|
||||
app.scroll_log_up();
|
||||
}
|
||||
KeyCode::PageDown => {
|
||||
app.log_scroll = app.log_scroll.saturating_add(10);
|
||||
}
|
||||
KeyCode::PageUp => {
|
||||
app.log_scroll = app.log_scroll.saturating_sub(10);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1003
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user