Add MergerFS pool display to dashboard matching CLAUDE.md format
All checks were successful
Build and Release / build-and-release (push) Successful in 2m32s
All checks were successful
Build and Release / build-and-release (push) Successful in 2m32s
Updated the dashboard system widget to properly display MergerFS storage pools in the exact format described in CLAUDE.md: - Pool header showing "mergerfs (2+1):" format - Total usage line: "├─ Total: ● 63% 2355.2GB/3686.4GB" - Data Disks section with tree structure - Individual drive entries: "│ ├─ ● sdb T: 24°C W: 5%" - Parity drives section: "├─ Parity: ● sdc T: 24°C W: 5%" - Mount point footer: "└─ Mount: /srv/media" The dashboard now processes both data_drives and parity_drives arrays from the agent data correctly and renders the complete MergerFS pool hierarchy with proper status indicators, temperatures, and wear levels. Storage display now matches the enhanced tree structure format specified in documentation with correct Unicode tree characters and spacing.
This commit is contained in:
parent
7b11db990c
commit
7a95a9d762
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -279,7 +279,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
|
||||
|
||||
[[package]]
|
||||
name = "cm-dashboard"
|
||||
version = "0.1.150"
|
||||
version = "0.1.151"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@ -301,7 +301,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cm-dashboard-agent"
|
||||
version = "0.1.150"
|
||||
version = "0.1.151"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -324,7 +324,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cm-dashboard-shared"
|
||||
version = "0.1.150"
|
||||
version = "0.1.151"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"serde",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "cm-dashboard-agent"
|
||||
version = "0.1.151"
|
||||
version = "0.1.152"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "cm-dashboard"
|
||||
version = "0.1.151"
|
||||
version = "0.1.152"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -57,7 +57,9 @@ struct StoragePool {
|
||||
name: String,
|
||||
mount_point: String,
|
||||
pool_type: String, // "single", "mergerfs (2+1)", "RAID5 (3+1)", etc.
|
||||
drives: Vec<StorageDrive>,
|
||||
drives: Vec<StorageDrive>, // For physical drives
|
||||
data_drives: Vec<StorageDrive>, // For MergerFS pools
|
||||
parity_drives: Vec<StorageDrive>, // For MergerFS pools
|
||||
filesystems: Vec<FileSystem>, // For physical drive pools: individual filesystem children
|
||||
usage_percent: Option<f32>,
|
||||
used_gb: Option<f32>,
|
||||
@ -227,6 +229,8 @@ impl SystemWidget {
|
||||
mount_point: drive.name.clone(),
|
||||
pool_type: "drive".to_string(),
|
||||
drives: Vec::new(),
|
||||
data_drives: Vec::new(),
|
||||
parity_drives: Vec::new(),
|
||||
filesystems: Vec::new(),
|
||||
usage_percent: None,
|
||||
used_gb: None,
|
||||
@ -267,7 +271,46 @@ impl SystemWidget {
|
||||
pools.insert(drive.name.clone(), pool);
|
||||
}
|
||||
|
||||
// Convert pools
|
||||
// Convert pools (MergerFS, RAID, etc.)
|
||||
for pool in &agent_data.system.storage.pools {
|
||||
let mut storage_pool = StoragePool {
|
||||
name: pool.name.clone(),
|
||||
mount_point: pool.mount.clone(),
|
||||
pool_type: pool.pool_type.clone(),
|
||||
drives: Vec::new(),
|
||||
data_drives: Vec::new(),
|
||||
parity_drives: Vec::new(),
|
||||
filesystems: Vec::new(),
|
||||
usage_percent: Some(pool.usage_percent),
|
||||
used_gb: Some(pool.used_gb),
|
||||
total_gb: Some(pool.total_gb),
|
||||
status: Status::Ok, // TODO: map pool health to status
|
||||
};
|
||||
|
||||
// Add data drives
|
||||
for drive in &pool.data_drives {
|
||||
let storage_drive = StorageDrive {
|
||||
name: drive.name.clone(),
|
||||
temperature: drive.temperature_celsius,
|
||||
wear_percent: drive.wear_percent,
|
||||
status: Status::Ok, // TODO: map drive health to status
|
||||
};
|
||||
storage_pool.data_drives.push(storage_drive);
|
||||
}
|
||||
|
||||
// Add parity drives
|
||||
for drive in &pool.parity_drives {
|
||||
let storage_drive = StorageDrive {
|
||||
name: drive.name.clone(),
|
||||
temperature: drive.temperature_celsius,
|
||||
wear_percent: drive.wear_percent,
|
||||
status: Status::Ok, // TODO: map drive health to status
|
||||
};
|
||||
storage_pool.parity_drives.push(storage_drive);
|
||||
}
|
||||
|
||||
pools.insert(pool.name.clone(), storage_pool);
|
||||
}
|
||||
|
||||
// Store pools
|
||||
let mut pool_list: Vec<StoragePool> = pools.into_values().collect();
|
||||
@ -306,8 +349,8 @@ impl SystemWidget {
|
||||
pool.name.clone()
|
||||
}
|
||||
} else {
|
||||
// For mergerfs pools, show pool name with format
|
||||
format!("{} ({})", pool.mount_point, pool.pool_type)
|
||||
// For mergerfs pools, show pool name with format like "mergerfs (2+1):"
|
||||
format!("{}:", pool.pool_type)
|
||||
};
|
||||
|
||||
let pool_spans = StatusIcons::create_status_spans(pool.status.clone(), &pool_label);
|
||||
@ -336,30 +379,70 @@ impl SystemWidget {
|
||||
lines.push(Line::from(fs_spans));
|
||||
}
|
||||
} else {
|
||||
// For mergerfs pools, show data drives and parity drives in tree structure
|
||||
if !pool.drives.is_empty() {
|
||||
// Group drives by type based on naming conventions or show all as data drives
|
||||
let (data_drives, parity_drives): (Vec<_>, Vec<_>) = pool.drives.iter()
|
||||
.partition(|d| !d.name.contains("parity") && !d.name.starts_with("sdc"));
|
||||
// For mergerfs pools, show structure matching CLAUDE.md format:
|
||||
// ● mergerfs (2+1):
|
||||
// ├─ Total: ● 63% 2355.2GB/3686.4GB
|
||||
// ├─ Data Disks:
|
||||
// │ ├─ ● sdb T: 24°C W: 5%
|
||||
// │ └─ ● sdd T: 27°C W: 5%
|
||||
// ├─ Parity: ● sdc T: 24°C W: 5%
|
||||
// └─ Mount: /srv/media
|
||||
|
||||
if !data_drives.is_empty() {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(" ├─ Data Disks:", Typography::secondary())
|
||||
]));
|
||||
for (i, drive) in data_drives.iter().enumerate() {
|
||||
render_pool_drive(drive, i == data_drives.len() - 1 && parity_drives.is_empty(), &mut lines);
|
||||
}
|
||||
}
|
||||
// Pool total usage
|
||||
let total_text = format!("Total: {:.0}% {:.1}GB/{:.1}GB",
|
||||
pool.usage_percent.unwrap_or(0.0),
|
||||
pool.used_gb.unwrap_or(0.0),
|
||||
pool.total_gb.unwrap_or(0.0)
|
||||
);
|
||||
let mut total_spans = vec![
|
||||
Span::styled(" ├─ ", Typography::tree()),
|
||||
];
|
||||
total_spans.extend(StatusIcons::create_status_spans(Status::Ok, &total_text));
|
||||
lines.push(Line::from(total_spans));
|
||||
|
||||
if !parity_drives.is_empty() {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(" └─ Parity:", Typography::secondary())
|
||||
]));
|
||||
for (i, drive) in parity_drives.iter().enumerate() {
|
||||
render_pool_drive(drive, i == parity_drives.len() - 1, &mut lines);
|
||||
}
|
||||
// Data Disks section
|
||||
if !pool.data_drives.is_empty() {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(" ├─ Data Disks:", Typography::secondary())
|
||||
]));
|
||||
for (i, drive) in pool.data_drives.iter().enumerate() {
|
||||
let is_last = i == pool.data_drives.len() - 1;
|
||||
let tree_symbol = if is_last { " │ └─ " } else { " │ ├─ " };
|
||||
render_mergerfs_drive(drive, tree_symbol, &mut lines);
|
||||
}
|
||||
}
|
||||
|
||||
// Parity section
|
||||
if !pool.parity_drives.is_empty() {
|
||||
let parity_symbol = " ├─ Parity: ";
|
||||
for drive in &pool.parity_drives {
|
||||
let mut drive_details = Vec::new();
|
||||
if let Some(temp) = drive.temperature {
|
||||
drive_details.push(format!("T: {}°C", temp as i32));
|
||||
}
|
||||
if let Some(wear) = drive.wear_percent {
|
||||
drive_details.push(format!("W: {}%", wear as i32));
|
||||
}
|
||||
|
||||
let drive_text = if !drive_details.is_empty() {
|
||||
format!("{} {}", drive.name, drive_details.join(" "))
|
||||
} else {
|
||||
drive.name.clone()
|
||||
};
|
||||
|
||||
let mut parity_spans = vec![
|
||||
Span::styled(parity_symbol, Typography::tree()),
|
||||
];
|
||||
parity_spans.extend(StatusIcons::create_status_spans(drive.status.clone(), &drive_text));
|
||||
lines.push(Line::from(parity_spans));
|
||||
}
|
||||
}
|
||||
|
||||
// Mount point
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(" └─ Mount: ", Typography::tree()),
|
||||
Span::styled(&pool.mount_point, Typography::secondary())
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
@ -367,6 +450,29 @@ impl SystemWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to render a drive in a MergerFS pool
|
||||
fn render_mergerfs_drive<'a>(drive: &StorageDrive, tree_symbol: &'a str, lines: &mut Vec<Line<'a>>) {
|
||||
let mut drive_details = Vec::new();
|
||||
if let Some(temp) = drive.temperature {
|
||||
drive_details.push(format!("T: {}°C", temp as i32));
|
||||
}
|
||||
if let Some(wear) = drive.wear_percent {
|
||||
drive_details.push(format!("W: {}%", wear as i32));
|
||||
}
|
||||
|
||||
let drive_text = if !drive_details.is_empty() {
|
||||
format!("{} {}", drive.name, drive_details.join(" "))
|
||||
} else {
|
||||
drive.name.clone()
|
||||
};
|
||||
|
||||
let mut drive_spans = vec![
|
||||
Span::styled(tree_symbol, Typography::tree()),
|
||||
];
|
||||
drive_spans.extend(StatusIcons::create_status_spans(drive.status.clone(), &drive_text));
|
||||
lines.push(Line::from(drive_spans));
|
||||
}
|
||||
|
||||
/// Helper function to render a drive in a storage pool
|
||||
fn render_pool_drive(drive: &StorageDrive, is_last: bool, lines: &mut Vec<Line<'_>>) {
|
||||
let tree_symbol = if is_last { " └─" } else { " ├─" };
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "cm-dashboard-shared"
|
||||
version = "0.1.151"
|
||||
version = "0.1.152"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user