Fix dashboard storage pool label styling
All checks were successful
Build and Release / build-and-release (push) Successful in 1m20s
All checks were successful
Build and Release / build-and-release (push) Successful in 1m20s
Replace non-existent Typography::primary() with Typography::secondary() for MergerFS pool labels following existing UI patterns.
This commit is contained in:
parent
7a95a9d762
commit
8f80015273
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -279,7 +279,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cm-dashboard"
|
name = "cm-dashboard"
|
||||||
version = "0.1.151"
|
version = "0.1.153"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -301,7 +301,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cm-dashboard-agent"
|
name = "cm-dashboard-agent"
|
||||||
version = "0.1.151"
|
version = "0.1.153"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -324,7 +324,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cm-dashboard-shared"
|
name = "cm-dashboard-shared"
|
||||||
version = "0.1.151"
|
version = "0.1.153"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cm-dashboard-agent"
|
name = "cm-dashboard-agent"
|
||||||
version = "0.1.152"
|
version = "0.1.153"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -76,11 +76,17 @@ impl DiskCollector {
|
|||||||
let mount_devices = self.get_mount_devices().await?;
|
let mount_devices = self.get_mount_devices().await?;
|
||||||
|
|
||||||
// Step 2: Get filesystem usage for each mount point using df
|
// Step 2: Get filesystem usage for each mount point using df
|
||||||
let filesystem_usage = self.get_filesystem_usage(&mount_devices).map_err(|e| CollectorError::Parse {
|
let mut filesystem_usage = self.get_filesystem_usage(&mount_devices).map_err(|e| CollectorError::Parse {
|
||||||
value: "filesystem usage".to_string(),
|
value: "filesystem usage".to_string(),
|
||||||
error: format!("Failed to get filesystem usage: {}", e),
|
error: format!("Failed to get filesystem usage: {}", e),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
// Step 2.5: Add MergerFS mount points that weren't in lsblk output
|
||||||
|
self.add_mergerfs_filesystem_usage(&mut filesystem_usage).map_err(|e| CollectorError::Parse {
|
||||||
|
value: "mergerfs filesystem usage".to_string(),
|
||||||
|
error: format!("Failed to get mergerfs filesystem usage: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
// Step 3: Detect MergerFS pools
|
// Step 3: Detect MergerFS pools
|
||||||
let mergerfs_pools = self.detect_mergerfs_pools(&filesystem_usage).map_err(|e| CollectorError::Parse {
|
let mergerfs_pools = self.detect_mergerfs_pools(&filesystem_usage).map_err(|e| CollectorError::Parse {
|
||||||
value: "mergerfs pools".to_string(),
|
value: "mergerfs pools".to_string(),
|
||||||
@ -156,6 +162,30 @@ impl DiskCollector {
|
|||||||
Ok(filesystem_usage)
|
Ok(filesystem_usage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add filesystem usage for MergerFS mount points that aren't in lsblk
|
||||||
|
fn add_mergerfs_filesystem_usage(&self, filesystem_usage: &mut HashMap<String, (u64, u64)>) -> anyhow::Result<()> {
|
||||||
|
let mounts_content = std::fs::read_to_string("/proc/mounts")
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to read /proc/mounts: {}", e))?;
|
||||||
|
|
||||||
|
for line in mounts_content.lines() {
|
||||||
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||||
|
if parts.len() >= 3 && parts[2] == "fuse.mergerfs" {
|
||||||
|
let mount_point = parts[1].to_string();
|
||||||
|
|
||||||
|
// Only add if we don't already have usage data for this mount point
|
||||||
|
if !filesystem_usage.contains_key(&mount_point) {
|
||||||
|
if let Ok((total, used)) = self.get_filesystem_info(&mount_point) {
|
||||||
|
debug!("Added MergerFS filesystem usage for {}: {}GB total, {}GB used",
|
||||||
|
mount_point, total as f32 / (1024.0 * 1024.0 * 1024.0), used as f32 / (1024.0 * 1024.0 * 1024.0));
|
||||||
|
filesystem_usage.insert(mount_point, (total, used));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Get filesystem info for a single mount point
|
/// Get filesystem info for a single mount point
|
||||||
fn get_filesystem_info(&self, mount_point: &str) -> Result<(u64, u64), CollectorError> {
|
fn get_filesystem_info(&self, mount_point: &str) -> Result<(u64, u64), CollectorError> {
|
||||||
let output = Command::new("df")
|
let output = Command::new("df")
|
||||||
@ -511,8 +541,8 @@ impl DiskCollector {
|
|||||||
/// Populate pools data into AgentData
|
/// Populate pools data into AgentData
|
||||||
fn populate_pools_data(&self, mergerfs_pools: &[MergerfsPool], smart_data: &HashMap<String, SmartData>, agent_data: &mut AgentData) -> Result<(), CollectorError> {
|
fn populate_pools_data(&self, mergerfs_pools: &[MergerfsPool], smart_data: &HashMap<String, SmartData>, agent_data: &mut AgentData) -> Result<(), CollectorError> {
|
||||||
for pool in mergerfs_pools {
|
for pool in mergerfs_pools {
|
||||||
// Calculate pool health based on member drive health
|
// Calculate pool health and statuses based on member drive health
|
||||||
let (pool_health, data_drive_data, parity_drive_data) = self.calculate_pool_health(pool, smart_data);
|
let (pool_health, health_status, usage_status, data_drive_data, parity_drive_data) = self.calculate_pool_health(pool, smart_data);
|
||||||
|
|
||||||
let pool_data = PoolData {
|
let pool_data = PoolData {
|
||||||
name: pool.name.clone(),
|
name: pool.name.clone(),
|
||||||
@ -526,6 +556,8 @@ impl DiskCollector {
|
|||||||
total_gb: pool.total_bytes as f32 / (1024.0 * 1024.0 * 1024.0),
|
total_gb: pool.total_bytes as f32 / (1024.0 * 1024.0 * 1024.0),
|
||||||
data_drives: data_drive_data,
|
data_drives: data_drive_data,
|
||||||
parity_drives: parity_drive_data,
|
parity_drives: parity_drive_data,
|
||||||
|
health_status,
|
||||||
|
usage_status,
|
||||||
};
|
};
|
||||||
|
|
||||||
agent_data.system.storage.pools.push(pool_data);
|
agent_data.system.storage.pools.push(pool_data);
|
||||||
@ -535,7 +567,7 @@ impl DiskCollector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate pool health based on member drive status
|
/// Calculate pool health based on member drive status
|
||||||
fn calculate_pool_health(&self, pool: &MergerfsPool, smart_data: &HashMap<String, SmartData>) -> (String, Vec<cm_dashboard_shared::PoolDriveData>, Vec<cm_dashboard_shared::PoolDriveData>) {
|
fn calculate_pool_health(&self, pool: &MergerfsPool, smart_data: &HashMap<String, SmartData>) -> (String, cm_dashboard_shared::Status, cm_dashboard_shared::Status, Vec<cm_dashboard_shared::PoolDriveData>, Vec<cm_dashboard_shared::PoolDriveData>) {
|
||||||
let mut failed_data = 0;
|
let mut failed_data = 0;
|
||||||
let mut failed_parity = 0;
|
let mut failed_parity = 0;
|
||||||
|
|
||||||
@ -543,16 +575,23 @@ impl DiskCollector {
|
|||||||
let data_drive_data: Vec<cm_dashboard_shared::PoolDriveData> = pool.data_drives.iter().map(|d| {
|
let data_drive_data: Vec<cm_dashboard_shared::PoolDriveData> = pool.data_drives.iter().map(|d| {
|
||||||
let smart = smart_data.get(&d.name);
|
let smart = smart_data.get(&d.name);
|
||||||
let health = smart.map(|s| s.health.clone()).unwrap_or_else(|| "UNKNOWN".to_string());
|
let health = smart.map(|s| s.health.clone()).unwrap_or_else(|| "UNKNOWN".to_string());
|
||||||
|
let temperature = smart.and_then(|s| s.temperature_celsius).or(d.temperature_celsius);
|
||||||
|
|
||||||
if health == "FAILED" {
|
if health == "FAILED" {
|
||||||
failed_data += 1;
|
failed_data += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate drive statuses using config thresholds
|
||||||
|
let health_status = self.calculate_health_status(&health);
|
||||||
|
let temperature_status = temperature.map(|t| self.temperature_thresholds.evaluate(t)).unwrap_or(cm_dashboard_shared::Status::Unknown);
|
||||||
|
|
||||||
cm_dashboard_shared::PoolDriveData {
|
cm_dashboard_shared::PoolDriveData {
|
||||||
name: d.name.clone(),
|
name: d.name.clone(),
|
||||||
temperature_celsius: smart.and_then(|s| s.temperature_celsius).or(d.temperature_celsius),
|
temperature_celsius: temperature,
|
||||||
health,
|
health,
|
||||||
wear_percent: smart.and_then(|s| s.wear_percent),
|
wear_percent: smart.and_then(|s| s.wear_percent),
|
||||||
|
health_status,
|
||||||
|
temperature_status,
|
||||||
}
|
}
|
||||||
}).collect();
|
}).collect();
|
||||||
|
|
||||||
@ -560,27 +599,47 @@ impl DiskCollector {
|
|||||||
let parity_drive_data: Vec<cm_dashboard_shared::PoolDriveData> = pool.parity_drives.iter().map(|d| {
|
let parity_drive_data: Vec<cm_dashboard_shared::PoolDriveData> = pool.parity_drives.iter().map(|d| {
|
||||||
let smart = smart_data.get(&d.name);
|
let smart = smart_data.get(&d.name);
|
||||||
let health = smart.map(|s| s.health.clone()).unwrap_or_else(|| "UNKNOWN".to_string());
|
let health = smart.map(|s| s.health.clone()).unwrap_or_else(|| "UNKNOWN".to_string());
|
||||||
|
let temperature = smart.and_then(|s| s.temperature_celsius).or(d.temperature_celsius);
|
||||||
|
|
||||||
if health == "FAILED" {
|
if health == "FAILED" {
|
||||||
failed_parity += 1;
|
failed_parity += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate drive statuses using config thresholds
|
||||||
|
let health_status = self.calculate_health_status(&health);
|
||||||
|
let temperature_status = temperature.map(|t| self.temperature_thresholds.evaluate(t)).unwrap_or(cm_dashboard_shared::Status::Unknown);
|
||||||
|
|
||||||
cm_dashboard_shared::PoolDriveData {
|
cm_dashboard_shared::PoolDriveData {
|
||||||
name: d.name.clone(),
|
name: d.name.clone(),
|
||||||
temperature_celsius: smart.and_then(|s| s.temperature_celsius).or(d.temperature_celsius),
|
temperature_celsius: temperature,
|
||||||
health,
|
health,
|
||||||
wear_percent: smart.and_then(|s| s.wear_percent),
|
wear_percent: smart.and_then(|s| s.wear_percent),
|
||||||
|
health_status,
|
||||||
|
temperature_status,
|
||||||
}
|
}
|
||||||
}).collect();
|
}).collect();
|
||||||
|
|
||||||
// Calculate overall pool health
|
// Calculate overall pool health string and status
|
||||||
let pool_health = match (failed_data, failed_parity) {
|
let (pool_health, health_status) = match (failed_data, failed_parity) {
|
||||||
(0, 0) => "healthy".to_string(),
|
(0, 0) => ("healthy".to_string(), cm_dashboard_shared::Status::Ok),
|
||||||
(1, 0) | (0, 1) => "degraded".to_string(), // One failure is degraded but recoverable
|
(1, 0) | (0, 1) => ("degraded".to_string(), cm_dashboard_shared::Status::Warning),
|
||||||
_ => "critical".to_string(), // Multiple failures are critical
|
_ => ("critical".to_string(), cm_dashboard_shared::Status::Critical),
|
||||||
};
|
};
|
||||||
|
|
||||||
(pool_health, data_drive_data, parity_drive_data)
|
// Calculate pool usage status using config thresholds
|
||||||
|
let usage_percent = if pool.total_bytes > 0 {
|
||||||
|
(pool.used_bytes as f32 / pool.total_bytes as f32) * 100.0
|
||||||
|
} else { 0.0 };
|
||||||
|
|
||||||
|
let usage_status = if usage_percent >= self.config.usage_critical_percent {
|
||||||
|
cm_dashboard_shared::Status::Critical
|
||||||
|
} else if usage_percent >= self.config.usage_warning_percent {
|
||||||
|
cm_dashboard_shared::Status::Warning
|
||||||
|
} else {
|
||||||
|
cm_dashboard_shared::Status::Ok
|
||||||
|
};
|
||||||
|
|
||||||
|
(pool_health, health_status, usage_status, data_drive_data, parity_drive_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate filesystem usage status
|
/// Calculate filesystem usage status
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cm-dashboard"
|
name = "cm-dashboard"
|
||||||
version = "0.1.152"
|
version = "0.1.153"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -273,6 +273,17 @@ impl SystemWidget {
|
|||||||
|
|
||||||
// Convert pools (MergerFS, RAID, etc.)
|
// Convert pools (MergerFS, RAID, etc.)
|
||||||
for pool in &agent_data.system.storage.pools {
|
for pool in &agent_data.system.storage.pools {
|
||||||
|
// Use agent-calculated status (combined health and usage status)
|
||||||
|
let pool_status = if pool.health_status == Status::Critical || pool.usage_status == Status::Critical {
|
||||||
|
Status::Critical
|
||||||
|
} else if pool.health_status == Status::Warning || pool.usage_status == Status::Warning {
|
||||||
|
Status::Warning
|
||||||
|
} else if pool.health_status == Status::Ok && pool.usage_status == Status::Ok {
|
||||||
|
Status::Ok
|
||||||
|
} else {
|
||||||
|
Status::Unknown
|
||||||
|
};
|
||||||
|
|
||||||
let mut storage_pool = StoragePool {
|
let mut storage_pool = StoragePool {
|
||||||
name: pool.name.clone(),
|
name: pool.name.clone(),
|
||||||
mount_point: pool.mount.clone(),
|
mount_point: pool.mount.clone(),
|
||||||
@ -284,27 +295,49 @@ impl SystemWidget {
|
|||||||
usage_percent: Some(pool.usage_percent),
|
usage_percent: Some(pool.usage_percent),
|
||||||
used_gb: Some(pool.used_gb),
|
used_gb: Some(pool.used_gb),
|
||||||
total_gb: Some(pool.total_gb),
|
total_gb: Some(pool.total_gb),
|
||||||
status: Status::Ok, // TODO: map pool health to status
|
status: pool_status,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add data drives
|
// Add data drives - use agent-calculated status
|
||||||
for drive in &pool.data_drives {
|
for drive in &pool.data_drives {
|
||||||
|
// Use combined health and temperature status
|
||||||
|
let drive_status = if drive.health_status == Status::Critical || drive.temperature_status == Status::Critical {
|
||||||
|
Status::Critical
|
||||||
|
} else if drive.health_status == Status::Warning || drive.temperature_status == Status::Warning {
|
||||||
|
Status::Warning
|
||||||
|
} else if drive.health_status == Status::Ok && drive.temperature_status == Status::Ok {
|
||||||
|
Status::Ok
|
||||||
|
} else {
|
||||||
|
Status::Unknown
|
||||||
|
};
|
||||||
|
|
||||||
let storage_drive = StorageDrive {
|
let storage_drive = StorageDrive {
|
||||||
name: drive.name.clone(),
|
name: drive.name.clone(),
|
||||||
temperature: drive.temperature_celsius,
|
temperature: drive.temperature_celsius,
|
||||||
wear_percent: drive.wear_percent,
|
wear_percent: drive.wear_percent,
|
||||||
status: Status::Ok, // TODO: map drive health to status
|
status: drive_status,
|
||||||
};
|
};
|
||||||
storage_pool.data_drives.push(storage_drive);
|
storage_pool.data_drives.push(storage_drive);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add parity drives
|
// Add parity drives - use agent-calculated status
|
||||||
for drive in &pool.parity_drives {
|
for drive in &pool.parity_drives {
|
||||||
|
// Use combined health and temperature status
|
||||||
|
let drive_status = if drive.health_status == Status::Critical || drive.temperature_status == Status::Critical {
|
||||||
|
Status::Critical
|
||||||
|
} else if drive.health_status == Status::Warning || drive.temperature_status == Status::Warning {
|
||||||
|
Status::Warning
|
||||||
|
} else if drive.health_status == Status::Ok && drive.temperature_status == Status::Ok {
|
||||||
|
Status::Ok
|
||||||
|
} else {
|
||||||
|
Status::Unknown
|
||||||
|
};
|
||||||
|
|
||||||
let storage_drive = StorageDrive {
|
let storage_drive = StorageDrive {
|
||||||
name: drive.name.clone(),
|
name: drive.name.clone(),
|
||||||
temperature: drive.temperature_celsius,
|
temperature: drive.temperature_celsius,
|
||||||
wear_percent: drive.wear_percent,
|
wear_percent: drive.wear_percent,
|
||||||
status: Status::Ok, // TODO: map drive health to status
|
status: drive_status,
|
||||||
};
|
};
|
||||||
storage_pool.parity_drives.push(storage_drive);
|
storage_pool.parity_drives.push(storage_drive);
|
||||||
}
|
}
|
||||||
@ -403,7 +436,8 @@ impl SystemWidget {
|
|||||||
// Data Disks section
|
// Data Disks section
|
||||||
if !pool.data_drives.is_empty() {
|
if !pool.data_drives.is_empty() {
|
||||||
lines.push(Line::from(vec![
|
lines.push(Line::from(vec![
|
||||||
Span::styled(" ├─ Data Disks:", Typography::secondary())
|
Span::styled(" ├─ ", Typography::tree()),
|
||||||
|
Span::styled("Data Disks:", Typography::secondary())
|
||||||
]));
|
]));
|
||||||
for (i, drive) in pool.data_drives.iter().enumerate() {
|
for (i, drive) in pool.data_drives.iter().enumerate() {
|
||||||
let is_last = i == pool.data_drives.len() - 1;
|
let is_last = i == pool.data_drives.len() - 1;
|
||||||
@ -414,7 +448,6 @@ impl SystemWidget {
|
|||||||
|
|
||||||
// Parity section
|
// Parity section
|
||||||
if !pool.parity_drives.is_empty() {
|
if !pool.parity_drives.is_empty() {
|
||||||
let parity_symbol = " ├─ Parity: ";
|
|
||||||
for drive in &pool.parity_drives {
|
for drive in &pool.parity_drives {
|
||||||
let mut drive_details = Vec::new();
|
let mut drive_details = Vec::new();
|
||||||
if let Some(temp) = drive.temperature {
|
if let Some(temp) = drive.temperature {
|
||||||
@ -431,7 +464,8 @@ impl SystemWidget {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut parity_spans = vec![
|
let mut parity_spans = vec![
|
||||||
Span::styled(parity_symbol, Typography::tree()),
|
Span::styled(" ├─ ", Typography::tree()),
|
||||||
|
Span::styled("Parity: ", Typography::secondary()),
|
||||||
];
|
];
|
||||||
parity_spans.extend(StatusIcons::create_status_spans(drive.status.clone(), &drive_text));
|
parity_spans.extend(StatusIcons::create_status_spans(drive.status.clone(), &drive_text));
|
||||||
lines.push(Line::from(parity_spans));
|
lines.push(Line::from(parity_spans));
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cm-dashboard-shared"
|
name = "cm-dashboard-shared"
|
||||||
version = "0.1.152"
|
version = "0.1.153"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -96,6 +96,8 @@ pub struct PoolData {
|
|||||||
pub total_gb: f32,
|
pub total_gb: f32,
|
||||||
pub data_drives: Vec<PoolDriveData>,
|
pub data_drives: Vec<PoolDriveData>,
|
||||||
pub parity_drives: Vec<PoolDriveData>,
|
pub parity_drives: Vec<PoolDriveData>,
|
||||||
|
pub health_status: Status,
|
||||||
|
pub usage_status: Status,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drive in a storage pool
|
/// Drive in a storage pool
|
||||||
@ -105,6 +107,8 @@ pub struct PoolDriveData {
|
|||||||
pub temperature_celsius: Option<f32>,
|
pub temperature_celsius: Option<f32>,
|
||||||
pub wear_percent: Option<f32>,
|
pub wear_percent: Option<f32>,
|
||||||
pub health: String,
|
pub health: String,
|
||||||
|
pub health_status: Status,
|
||||||
|
pub temperature_status: Status,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Service monitoring data
|
/// Service monitoring data
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user