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] 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:**
|
||||
- CPU load thresholds: Warning ≥ 9.0, Critical ≥ 10.0
|
||||
- CPU temperature thresholds: Warning ≥ 100°C, Critical ≥ 100°C (effectively disabled)
|
||||
|
||||
@ -538,22 +538,94 @@ impl TuiApp {
|
||||
|
||||
let mut chunk_index = 0;
|
||||
|
||||
// Display each storage pool
|
||||
// Display each storage pool with tree structure
|
||||
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" {
|
||||
"root".to_string()
|
||||
} else {
|
||||
pool_name.clone()
|
||||
};
|
||||
|
||||
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());
|
||||
frame.render_widget(pool_title_para, content_chunks[chunk_index]);
|
||||
let pool_type = if drives.len() > 1 { "multi-drive" } else { "Single" };
|
||||
|
||||
// Get pool status from usage metric
|
||||
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;
|
||||
|
||||
// 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
|
||||
.get_metric(hostname, &format!("disk_{}_usage_percent", pool_name))
|
||||
.and_then(|m| m.value.as_f32())
|
||||
@ -574,58 +646,37 @@ impl TuiApp {
|
||||
.map(|m| m.status)
|
||||
.unwrap_or(Status::Unknown);
|
||||
|
||||
let usage_spans = StatusIcons::create_status_spans(
|
||||
usage_status,
|
||||
&format!(
|
||||
"Usage: {:.1}% • {:.1}/{:.1} GB",
|
||||
usage_percent, used_gb, total_gb
|
||||
// Format usage with proper units
|
||||
let (used_display, total_display, unit) = if total_gb < 1.0 {
|
||||
(used_gb * 1024.0, total_gb * 1024.0, "MB")
|
||||
} else {
|
||||
(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));
|
||||
frame.render_widget(usage_para, content_chunks[chunk_index]);
|
||||
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user