Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a95a9d762 |
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.150"
|
version = "0.1.151"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -301,7 +301,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cm-dashboard-agent"
|
name = "cm-dashboard-agent"
|
||||||
version = "0.1.150"
|
version = "0.1.151"
|
||||||
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.150"
|
version = "0.1.151"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cm-dashboard-agent"
|
name = "cm-dashboard-agent"
|
||||||
version = "0.1.151"
|
version = "0.1.152"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cm-dashboard"
|
name = "cm-dashboard"
|
||||||
version = "0.1.151"
|
version = "0.1.152"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -57,7 +57,9 @@ struct StoragePool {
|
|||||||
name: String,
|
name: String,
|
||||||
mount_point: String,
|
mount_point: String,
|
||||||
pool_type: String, // "single", "mergerfs (2+1)", "RAID5 (3+1)", etc.
|
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
|
filesystems: Vec<FileSystem>, // For physical drive pools: individual filesystem children
|
||||||
usage_percent: Option<f32>,
|
usage_percent: Option<f32>,
|
||||||
used_gb: Option<f32>,
|
used_gb: Option<f32>,
|
||||||
@@ -227,6 +229,8 @@ impl SystemWidget {
|
|||||||
mount_point: drive.name.clone(),
|
mount_point: drive.name.clone(),
|
||||||
pool_type: "drive".to_string(),
|
pool_type: "drive".to_string(),
|
||||||
drives: Vec::new(),
|
drives: Vec::new(),
|
||||||
|
data_drives: Vec::new(),
|
||||||
|
parity_drives: Vec::new(),
|
||||||
filesystems: Vec::new(),
|
filesystems: Vec::new(),
|
||||||
usage_percent: None,
|
usage_percent: None,
|
||||||
used_gb: None,
|
used_gb: None,
|
||||||
@@ -267,7 +271,46 @@ impl SystemWidget {
|
|||||||
pools.insert(drive.name.clone(), pool);
|
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
|
// Store pools
|
||||||
let mut pool_list: Vec<StoragePool> = pools.into_values().collect();
|
let mut pool_list: Vec<StoragePool> = pools.into_values().collect();
|
||||||
@@ -306,8 +349,8 @@ impl SystemWidget {
|
|||||||
pool.name.clone()
|
pool.name.clone()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// For mergerfs pools, show pool name with format
|
// For mergerfs pools, show pool name with format like "mergerfs (2+1):"
|
||||||
format!("{} ({})", pool.mount_point, pool.pool_type)
|
format!("{}:", pool.pool_type)
|
||||||
};
|
};
|
||||||
|
|
||||||
let pool_spans = StatusIcons::create_status_spans(pool.status.clone(), &pool_label);
|
let pool_spans = StatusIcons::create_status_spans(pool.status.clone(), &pool_label);
|
||||||
@@ -336,30 +379,70 @@ impl SystemWidget {
|
|||||||
lines.push(Line::from(fs_spans));
|
lines.push(Line::from(fs_spans));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// For mergerfs pools, show data drives and parity drives in tree structure
|
// For mergerfs pools, show structure matching CLAUDE.md format:
|
||||||
if !pool.drives.is_empty() {
|
// ● mergerfs (2+1):
|
||||||
// Group drives by type based on naming conventions or show all as data drives
|
// ├─ Total: ● 63% 2355.2GB/3686.4GB
|
||||||
let (data_drives, parity_drives): (Vec<_>, Vec<_>) = pool.drives.iter()
|
// ├─ Data Disks:
|
||||||
.partition(|d| !d.name.contains("parity") && !d.name.starts_with("sdc"));
|
// │ ├─ ● sdb T: 24°C W: 5%
|
||||||
|
// │ └─ ● sdd T: 27°C W: 5%
|
||||||
|
// ├─ Parity: ● sdc T: 24°C W: 5%
|
||||||
|
// └─ Mount: /srv/media
|
||||||
|
|
||||||
|
// 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 !data_drives.is_empty() {
|
// Data Disks section
|
||||||
lines.push(Line::from(vec![
|
if !pool.data_drives.is_empty() {
|
||||||
Span::styled(" ├─ Data Disks:", Typography::secondary())
|
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);
|
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);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
/// Helper function to render a drive in a storage pool
|
||||||
fn render_pool_drive(drive: &StorageDrive, is_last: bool, lines: &mut Vec<Line<'_>>) {
|
fn render_pool_drive(drive: &StorageDrive, is_last: bool, lines: &mut Vec<Line<'_>>) {
|
||||||
let tree_symbol = if is_last { " └─" } else { " ├─" };
|
let tree_symbol = if is_last { " └─" } else { " ├─" };
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cm-dashboard-shared"
|
name = "cm-dashboard-shared"
|
||||||
version = "0.1.151"
|
version = "0.1.152"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
Reference in New Issue
Block a user