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
+1
View File
@@ -0,0 +1 @@
/target
+2090
View File
File diff suppressed because it is too large Load Diff
+37
View File
@@ -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)'] }
+846
View File
@@ -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)
}
+2
View File
@@ -0,0 +1,2 @@
pub mod simple;
pub mod diagnostics;
+10
View File
@@ -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();
}
+3
View File
@@ -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
+7
View File
@@ -0,0 +1,7 @@
#[cfg(feature = "gui")]
pub mod api;
#[cfg(feature = "gui")]
mod frb_generated;
#[cfg(feature = "tui")]
pub mod tui;
+399
View File
@@ -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
}
+295
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff