Implement storage widget tree structure with themed status icons
Add proper hierarchical tree display for storage pools and drives: - Pool headers with status icons and type indication (Single/multi-drive) - Individual drive lines with ├─ tree symbols and health status - Usage summary with └─ end symbol and capacity status - T: and W: prefixes for temperature and wear level metrics - Themed status icons using StatusIcons::get_icon() with proper colors - 2-space indentation for clean tree structure appearance Replace flat storage display with beautiful tree format: ● Storage steampool (multi-drive): ├─ ● sdb T:35°C W:12% ├─ ● sdc T:38°C W:8% └─ ● 78.1% 1250.3GB/1600.0GB Uses agent-calculated status from NixOS-configured thresholds. Update CLAUDE.md with complete implementation specification.
This commit is contained in:
parent
1591565b1b
commit
b1f294cf2f
37
CLAUDE.md
37
CLAUDE.md
@ -397,6 +397,43 @@ Agent → ["cpu_load_1min", "memory_usage_percent", ...] → Dashboard → Widge
|
|||||||
- [x] Eliminated configuration drift between defaults and deployed settings
|
- [x] Eliminated configuration drift between defaults and deployed settings
|
||||||
- [x] All cm-dashboard configuration now managed declaratively through NixOS
|
- [x] All cm-dashboard configuration now managed declaratively through NixOS
|
||||||
|
|
||||||
|
**In Progress:**
|
||||||
|
- [ ] **Storage Widget Tree Structure Implementation (2025-10-22)**
|
||||||
|
- [ ] Replace flat storage display with proper tree structure format
|
||||||
|
- [ ] Implement themed status icons for pool/drive/usage status
|
||||||
|
- [ ] Add tree symbols (├─, └─) with proper indentation for hierarchical display
|
||||||
|
- [ ] Support T: and W: prefixes for temperature and wear metrics
|
||||||
|
- [ ] Use agent-calculated status from NixOS-configured thresholds (no dashboard calculations)
|
||||||
|
|
||||||
|
### Storage Widget Tree Structure Specification
|
||||||
|
|
||||||
|
**Target Display Format:**
|
||||||
|
```
|
||||||
|
● Storage steampool (Raid0):
|
||||||
|
├─ ● sdb T:35°C W:12%
|
||||||
|
├─ ● sdc T:38°C W:8%
|
||||||
|
└─ ● 78.1% 1250.3GB/1600.0GB
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status Icon Sources:**
|
||||||
|
- **Pool Status**: Aggregated status from pool health + usage (`disk_{pool}_usage_percent` metric status)
|
||||||
|
- **Drive Status**: Individual SMART health status (`disk_{pool}_{drive}_health` metric status)
|
||||||
|
- **Usage Status**: Disk usage level status (`disk_{pool}_usage_percent` metric status)
|
||||||
|
|
||||||
|
**Implementation Details:**
|
||||||
|
- **Tree Symbols**: `├─` for intermediate lines, `└─` for final line
|
||||||
|
- **Indentation**: 2 spaces before tree symbols
|
||||||
|
- **Status Icons**: Use `StatusIcons::get_icon(status)` with themed colors
|
||||||
|
- **Temperature Format**: `T:{temp}°C` from `disk_{pool}_{drive}_temperature` metrics
|
||||||
|
- **Wear Format**: `W:{wear}%` from `disk_{pool}_{drive}_wear_percent` metrics
|
||||||
|
- **Pool Type**: Determine from drive count (Single/multi-drive) or RAID type detection
|
||||||
|
- **Status Calculation**: Dashboard displays agent-calculated status, no threshold evaluation
|
||||||
|
|
||||||
|
**Layout Constraints:**
|
||||||
|
- Dynamic: 1 header + N drives + 1 usage line per storage pool
|
||||||
|
- Supports multiple storage pools with proper spacing
|
||||||
|
- Truncation indicator if pools exceed available display space
|
||||||
|
|
||||||
**Production Configuration:**
|
**Production Configuration:**
|
||||||
- CPU load thresholds: Warning ≥ 9.0, Critical ≥ 10.0
|
- CPU load thresholds: Warning ≥ 9.0, Critical ≥ 10.0
|
||||||
- CPU temperature thresholds: Warning ≥ 100°C, Critical ≥ 100°C (effectively disabled)
|
- CPU temperature thresholds: Warning ≥ 100°C, Critical ≥ 100°C (effectively disabled)
|
||||||
|
|||||||
@ -538,22 +538,94 @@ impl TuiApp {
|
|||||||
|
|
||||||
let mut chunk_index = 0;
|
let mut chunk_index = 0;
|
||||||
|
|
||||||
// Display each storage pool
|
// Display each storage pool with tree structure
|
||||||
for (pool_name, drives) in &pools_to_show {
|
for (pool_name, drives) in &pools_to_show {
|
||||||
// Pool title with type indicator
|
// Pool header with status icon and type
|
||||||
let pool_display_name = if pool_name == "root" {
|
let pool_display_name = if pool_name == "root" {
|
||||||
"root".to_string()
|
"root".to_string()
|
||||||
} else {
|
} else {
|
||||||
pool_name.clone()
|
pool_name.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
let pool_type = if drives.len() > 1 { "multi-drive" } else { "single" };
|
let pool_type = if drives.len() > 1 { "multi-drive" } else { "Single" };
|
||||||
let pool_title_text = format!("Storage {} ({}):", pool_display_name, pool_type);
|
|
||||||
let pool_title_para = Paragraph::new(pool_title_text).style(Typography::widget_title());
|
// Get pool status from usage metric
|
||||||
frame.render_widget(pool_title_para, content_chunks[chunk_index]);
|
let pool_status = metric_store
|
||||||
|
.get_metric(hostname, &format!("disk_{}_usage_percent", pool_name))
|
||||||
|
.map(|m| m.status)
|
||||||
|
.unwrap_or(Status::Unknown);
|
||||||
|
|
||||||
|
// Create pool header with status icon
|
||||||
|
let pool_status_icon = StatusIcons::get_icon(pool_status);
|
||||||
|
let pool_status_color = Theme::status_color(pool_status);
|
||||||
|
let pool_header_text = format!("Storage {} ({}):", pool_display_name, pool_type);
|
||||||
|
|
||||||
|
let pool_header_spans = vec![
|
||||||
|
ratatui::text::Span::styled(
|
||||||
|
format!("{} ", pool_status_icon),
|
||||||
|
Style::default().fg(pool_status_color),
|
||||||
|
),
|
||||||
|
ratatui::text::Span::styled(
|
||||||
|
pool_header_text,
|
||||||
|
Typography::widget_title(),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
let pool_header_para = Paragraph::new(ratatui::text::Line::from(pool_header_spans));
|
||||||
|
frame.render_widget(pool_header_para, content_chunks[chunk_index]);
|
||||||
chunk_index += 1;
|
chunk_index += 1;
|
||||||
|
|
||||||
// Pool usage information
|
// Individual drive lines with tree symbols
|
||||||
|
let mut sorted_drives = drives.clone();
|
||||||
|
sorted_drives.sort();
|
||||||
|
for (_drive_idx, drive_name) in sorted_drives.iter().enumerate() {
|
||||||
|
// Get drive health status
|
||||||
|
let drive_health_metric = metric_store
|
||||||
|
.get_metric(hostname, &format!("disk_{}_{}_health", pool_name, drive_name));
|
||||||
|
let drive_status = drive_health_metric
|
||||||
|
.map(|m| m.status)
|
||||||
|
.unwrap_or(Status::Unknown);
|
||||||
|
|
||||||
|
// Get drive temperature
|
||||||
|
let temp_text = metric_store
|
||||||
|
.get_metric(hostname, &format!("disk_{}_{}_temperature", pool_name, drive_name))
|
||||||
|
.and_then(|m| m.value.as_f32())
|
||||||
|
.map(|temp| format!(" T:{:.0}°C", temp))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
// Get drive wear level (SSDs)
|
||||||
|
let wear_text = metric_store
|
||||||
|
.get_metric(hostname, &format!("disk_{}_{}_wear_percent", pool_name, drive_name))
|
||||||
|
.and_then(|m| m.value.as_f32())
|
||||||
|
.map(|wear| format!(" W:{:.0}%", wear))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
// Build drive line with tree symbol
|
||||||
|
let tree_symbol = "├─";
|
||||||
|
let drive_status_icon = StatusIcons::get_icon(drive_status);
|
||||||
|
let drive_status_color = Theme::status_color(drive_status);
|
||||||
|
let drive_text = format!("{}{}{}", drive_name, temp_text, wear_text);
|
||||||
|
|
||||||
|
let drive_spans = vec![
|
||||||
|
ratatui::text::Span::styled(" ", Style::default()), // 2-space indentation
|
||||||
|
ratatui::text::Span::styled(
|
||||||
|
format!("{} ", tree_symbol),
|
||||||
|
Style::default().fg(Theme::muted_text()),
|
||||||
|
),
|
||||||
|
ratatui::text::Span::styled(
|
||||||
|
format!("{} ", drive_status_icon),
|
||||||
|
Style::default().fg(drive_status_color),
|
||||||
|
),
|
||||||
|
ratatui::text::Span::styled(
|
||||||
|
drive_text,
|
||||||
|
Style::default().fg(Theme::primary_text()),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
let drive_para = Paragraph::new(ratatui::text::Line::from(drive_spans));
|
||||||
|
frame.render_widget(drive_para, content_chunks[chunk_index]);
|
||||||
|
chunk_index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage line with end tree symbol and status icon
|
||||||
let usage_percent = metric_store
|
let usage_percent = metric_store
|
||||||
.get_metric(hostname, &format!("disk_{}_usage_percent", pool_name))
|
.get_metric(hostname, &format!("disk_{}_usage_percent", pool_name))
|
||||||
.and_then(|m| m.value.as_f32())
|
.and_then(|m| m.value.as_f32())
|
||||||
@ -574,58 +646,37 @@ impl TuiApp {
|
|||||||
.map(|m| m.status)
|
.map(|m| m.status)
|
||||||
.unwrap_or(Status::Unknown);
|
.unwrap_or(Status::Unknown);
|
||||||
|
|
||||||
let usage_spans = StatusIcons::create_status_spans(
|
// Format usage with proper units
|
||||||
usage_status,
|
let (used_display, total_display, unit) = if total_gb < 1.0 {
|
||||||
&format!(
|
(used_gb * 1024.0, total_gb * 1024.0, "MB")
|
||||||
"Usage: {:.1}% • {:.1}/{:.1} GB",
|
} else {
|
||||||
usage_percent, used_gb, total_gb
|
(used_gb, total_gb, "GB")
|
||||||
|
};
|
||||||
|
|
||||||
|
let end_tree_symbol = "└─";
|
||||||
|
let usage_status_icon = StatusIcons::get_icon(usage_status);
|
||||||
|
let usage_status_color = Theme::status_color(usage_status);
|
||||||
|
let usage_text = format!("{:.1}% {:.1}{}/{:.1}{}",
|
||||||
|
usage_percent, used_display, unit, total_display, unit);
|
||||||
|
|
||||||
|
let usage_spans = vec![
|
||||||
|
ratatui::text::Span::styled(" ", Style::default()), // 2-space indentation
|
||||||
|
ratatui::text::Span::styled(
|
||||||
|
format!("{} ", end_tree_symbol),
|
||||||
|
Style::default().fg(Theme::muted_text()),
|
||||||
),
|
),
|
||||||
);
|
ratatui::text::Span::styled(
|
||||||
|
format!("{} ", usage_status_icon),
|
||||||
|
Style::default().fg(usage_status_color),
|
||||||
|
),
|
||||||
|
ratatui::text::Span::styled(
|
||||||
|
usage_text,
|
||||||
|
Style::default().fg(Theme::primary_text()),
|
||||||
|
),
|
||||||
|
];
|
||||||
let usage_para = Paragraph::new(ratatui::text::Line::from(usage_spans));
|
let usage_para = Paragraph::new(ratatui::text::Line::from(usage_spans));
|
||||||
frame.render_widget(usage_para, content_chunks[chunk_index]);
|
frame.render_widget(usage_para, content_chunks[chunk_index]);
|
||||||
chunk_index += 1;
|
chunk_index += 1;
|
||||||
|
|
||||||
// Individual drive information
|
|
||||||
let mut sorted_drives = drives.clone();
|
|
||||||
sorted_drives.sort();
|
|
||||||
for drive_name in &sorted_drives {
|
|
||||||
// Get drive health
|
|
||||||
let health_metric = metric_store
|
|
||||||
.get_metric(hostname, &format!("disk_{}_{}_health", pool_name, drive_name));
|
|
||||||
let (health_status, health_color) = if let Some(metric) = health_metric {
|
|
||||||
(metric.value.as_string(), metric.status)
|
|
||||||
} else {
|
|
||||||
("Unknown".to_string(), Status::Unknown)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get drive temperature
|
|
||||||
let temp_text = metric_store
|
|
||||||
.get_metric(hostname, &format!("disk_{}_{}_temperature", pool_name, drive_name))
|
|
||||||
.and_then(|m| m.value.as_f32())
|
|
||||||
.map(|temp| format!(" {}°C", temp as i32))
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
// Get drive wear level (SSDs)
|
|
||||||
let wear_text = metric_store
|
|
||||||
.get_metric(hostname, &format!("disk_{}_{}_wear_percent", pool_name, drive_name))
|
|
||||||
.and_then(|m| m.value.as_f32())
|
|
||||||
.map(|wear| format!(" {}%", wear as i32))
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
// Build drive status line
|
|
||||||
let mut drive_info = format!("{}: {}", drive_name, health_status);
|
|
||||||
if !temp_text.is_empty() {
|
|
||||||
drive_info.push_str(&temp_text);
|
|
||||||
}
|
|
||||||
if !wear_text.is_empty() {
|
|
||||||
drive_info.push_str(&format!(" wear{}", wear_text));
|
|
||||||
}
|
|
||||||
|
|
||||||
let drive_spans = StatusIcons::create_status_spans(health_color, &drive_info);
|
|
||||||
let drive_para = Paragraph::new(ratatui::text::Line::from(drive_spans));
|
|
||||||
frame.render_widget(drive_para, content_chunks[chunk_index]);
|
|
||||||
chunk_index += 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show truncation indicator if we couldn't display all pools
|
// Show truncation indicator if we couldn't display all pools
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user