Compare commits

...

3 Commits

Author SHA1 Message Date
748a9f3a3b Move Network section below RAM in system widget
All checks were successful
Build and Release / build-and-release (push) Successful in 1m11s
Reordered display sections in system widget:
- Network section now appears after RAM and tmpfs mounts
- Improves logical grouping by placing network info between memory and storage
- Updated to version 0.1.168
2025-11-26 23:23:56 +01:00
5c6b11c794 Filter out network interfaces without IP addresses
All checks were successful
Build and Release / build-and-release (push) Successful in 1m9s
Remove interfaces like ifb0, dummy devices that have no IPs. Only show interfaces with at least one IPv4 or IPv6 address.

Version bump to 0.1.167
2025-11-26 19:19:21 +01:00
9f0aa5f806 Update network display format to match CLAUDE.md specification
All checks were successful
Build and Release / build-and-release (push) Successful in 1m38s
Nest IP addresses under physical interface names. Show physical interfaces with status icon on header line. Virtual interfaces show inline with compressed IPs.

Format:
● eno1:
  ├─ ip: 192.168.30.105
  └─ tailscale0: 100.125.108.16

Version bump to 0.1.166
2025-11-26 19:13:28 +01:00
7 changed files with 87 additions and 70 deletions

View File

@@ -304,6 +304,12 @@ exclude_fs_types = ["tmpfs", "devtmpfs", "sysfs", "proc"]
### Display Format ### Display Format
``` ```
Network:
● eno1:
├─ ip: 192.168.30.105
└─ tailscale0: 100.125.108.16
● eno2:
└─ ip: 192.168.32.105
CPU: CPU:
● Load: 0.23 0.21 0.13 ● Load: 0.23 0.21 0.13
└─ Freq: 1048 MHz └─ Freq: 1048 MHz

6
Cargo.lock generated
View File

@@ -279,7 +279,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
[[package]] [[package]]
name = "cm-dashboard" name = "cm-dashboard"
version = "0.1.165" version = "0.1.168"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -301,7 +301,7 @@ dependencies = [
[[package]] [[package]]
name = "cm-dashboard-agent" name = "cm-dashboard-agent"
version = "0.1.165" version = "0.1.168"
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.165" version = "0.1.168"
dependencies = [ dependencies = [
"chrono", "chrono",
"serde", "serde",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "cm-dashboard-agent" name = "cm-dashboard-agent"
version = "0.1.165" version = "0.1.168"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

View File

@@ -90,6 +90,12 @@ impl NetworkCollector {
} }
} }
// Only add interfaces that have at least one IP address
// This filters out ifb*, dummy interfaces, etc. that have no IPs
if ipv4_addresses.is_empty() && ipv6_addresses.is_empty() {
continue;
}
// Determine if physical and get status // Determine if physical and get status
let is_physical = Self::is_physical_interface(&name); let is_physical = Self::is_physical_interface(&name);
let link_status = if is_physical { let link_status = if is_physical {
@@ -104,7 +110,7 @@ impl NetworkCollector {
ipv6_addresses, ipv6_addresses,
is_physical, is_physical,
link_status, link_status,
parent_interface: None, // TODO: Implement virtual interface parent detection parent_interface: None,
}); });
} }
} }

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "cm-dashboard" name = "cm-dashboard"
version = "0.1.165" version = "0.1.168"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

View File

@@ -628,60 +628,65 @@ impl SystemWidget {
let physical: Vec<_> = self.network_interfaces.iter().filter(|i| i.is_physical).collect(); let physical: Vec<_> = self.network_interfaces.iter().filter(|i| i.is_physical).collect();
let virtual_interfaces: Vec<_> = self.network_interfaces.iter().filter(|i| !i.is_physical).collect(); let virtual_interfaces: Vec<_> = self.network_interfaces.iter().filter(|i| !i.is_physical).collect();
// Render physical interfaces first // Render physical interfaces
for (i, interface) in physical.iter().enumerate() { for (phy_idx, interface) in physical.iter().enumerate() {
let is_last = i == physical.len() - 1 && virtual_interfaces.is_empty(); let is_last_physical = phy_idx == physical.len() - 1 && virtual_interfaces.is_empty();
let tree_symbol = if is_last { " └─ " } else { " ├─ " };
// Show interface name with IPs // Physical interface header with status icon
let mut interface_text = format!("{}: ", interface.name); let mut header_spans = vec![];
header_spans.extend(StatusIcons::create_status_spans(
// Add compressed IPv4 addresses
if !interface.ipv4_addresses.is_empty() {
interface_text.push_str(&Self::compress_ipv4_addresses(&interface.ipv4_addresses));
}
// Add IPv6 addresses (no compression for now)
if !interface.ipv6_addresses.is_empty() {
if !interface.ipv4_addresses.is_empty() {
interface_text.push_str(", ");
}
interface_text.push_str(&interface.ipv6_addresses.join(", "));
}
// Physical interfaces show status icon
let mut spans = vec![
Span::styled(tree_symbol, Typography::tree()),
];
spans.extend(StatusIcons::create_status_spans(
interface.link_status.clone(), interface.link_status.clone(),
&interface_text &format!("{}:", interface.name)
)); ));
lines.push(Line::from(spans)); lines.push(Line::from(header_spans));
// Show IPs nested under the interface
let ip_count = interface.ipv4_addresses.len() + interface.ipv6_addresses.len();
let mut ip_index = 0;
// IPv4 addresses
for ipv4 in &interface.ipv4_addresses {
ip_index += 1;
let is_last_ip = ip_index == ip_count && is_last_physical;
let tree_symbol = if is_last_ip { " └─ " } else { " ├─ " };
lines.push(Line::from(vec![
Span::styled(tree_symbol, Typography::tree()),
Span::styled(format!("ip: {}", ipv4), Typography::secondary()),
]));
}
// IPv6 addresses
for ipv6 in &interface.ipv6_addresses {
ip_index += 1;
let is_last_ip = ip_index == ip_count && is_last_physical;
let tree_symbol = if is_last_ip { " └─ " } else { " ├─ " };
lines.push(Line::from(vec![
Span::styled(tree_symbol, Typography::tree()),
Span::styled(format!("ip: {}", ipv6), Typography::secondary()),
]));
}
} }
// Render virtual interfaces // Render standalone virtual interfaces (those without a parent)
for (i, interface) in virtual_interfaces.iter().enumerate() { for (virt_idx, interface) in virtual_interfaces.iter().enumerate() {
let is_last = i == virtual_interfaces.len() - 1; let is_last = virt_idx == virtual_interfaces.len() - 1;
let tree_symbol = if is_last { " └─ " } else { " ├─ " }; let tree_symbol = if is_last { " └─ " } else { " ├─ " };
// Show interface name with IPs // Virtual interface with IPs
let mut interface_text = format!("{}: ", interface.name); let ip_text = if !interface.ipv4_addresses.is_empty() {
Self::compress_ipv4_addresses(&interface.ipv4_addresses)
} else if !interface.ipv6_addresses.is_empty() {
interface.ipv6_addresses.join(", ")
} else {
String::new()
};
// Add compressed IPv4 addresses let interface_text = if !ip_text.is_empty() {
if !interface.ipv4_addresses.is_empty() { format!("{}: {}", interface.name, ip_text)
interface_text.push_str(&Self::compress_ipv4_addresses(&interface.ipv4_addresses)); } else {
} format!("{}:", interface.name)
};
// Add IPv6 addresses (no compression for now)
if !interface.ipv6_addresses.is_empty() {
if !interface.ipv4_addresses.is_empty() {
interface_text.push_str(", ");
}
interface_text.push_str(&interface.ipv6_addresses.join(", "));
}
// Virtual interfaces don't show status icon
lines.push(Line::from(vec![ lines.push(Line::from(vec![
Span::styled(tree_symbol, Typography::tree()), Span::styled(tree_symbol, Typography::tree()),
Span::styled(interface_text, Typography::secondary()), Span::styled(interface_text, Typography::secondary()),
@@ -710,28 +715,18 @@ impl SystemWidget {
Span::styled(format!("Agent: {}", agent_version_text), Typography::secondary()) Span::styled(format!("Agent: {}", agent_version_text), Typography::secondary())
])); ]));
// Network section
if !self.network_interfaces.is_empty() {
lines.push(Line::from(vec![
Span::styled("Network:", Typography::widget_title())
]));
let network_lines = self.render_network();
lines.extend(network_lines);
}
// CPU section // CPU section
lines.push(Line::from(vec![ lines.push(Line::from(vec![
Span::styled("CPU:", Typography::widget_title()) Span::styled("CPU:", Typography::widget_title())
])); ]));
let load_text = self.format_cpu_load(); let load_text = self.format_cpu_load();
let cpu_spans = StatusIcons::create_status_spans( let cpu_spans = StatusIcons::create_status_spans(
self.cpu_status.clone(), self.cpu_status.clone(),
&format!("Load: {}", load_text) &format!("Load: {}", load_text)
); );
lines.push(Line::from(cpu_spans)); lines.push(Line::from(cpu_spans));
let freq_text = self.format_cpu_frequency(); let freq_text = self.format_cpu_frequency();
lines.push(Line::from(vec![ lines.push(Line::from(vec![
Span::styled(" └─ ", Typography::tree()), Span::styled(" └─ ", Typography::tree()),
@@ -742,7 +737,7 @@ impl SystemWidget {
lines.push(Line::from(vec![ lines.push(Line::from(vec![
Span::styled("RAM:", Typography::widget_title()) Span::styled("RAM:", Typography::widget_title())
])); ]));
let memory_text = self.format_memory_usage(); let memory_text = self.format_memory_usage();
let memory_spans = StatusIcons::create_status_spans( let memory_spans = StatusIcons::create_status_spans(
self.memory_status.clone(), self.memory_status.clone(),
@@ -754,16 +749,16 @@ impl SystemWidget {
for (i, tmpfs) in self.tmpfs_mounts.iter().enumerate() { for (i, tmpfs) in self.tmpfs_mounts.iter().enumerate() {
let is_last = i == self.tmpfs_mounts.len() - 1; let is_last = i == self.tmpfs_mounts.len() - 1;
let tree_symbol = if is_last { " └─ " } else { " ├─ " }; let tree_symbol = if is_last { " └─ " } else { " ├─ " };
let usage_text = if tmpfs.total_gb > 0.0 { let usage_text = if tmpfs.total_gb > 0.0 {
format!("{:.0}% {:.1}GB/{:.1}GB", format!("{:.0}% {:.1}GB/{:.1}GB",
tmpfs.usage_percent, tmpfs.usage_percent,
tmpfs.used_gb, tmpfs.used_gb,
tmpfs.total_gb) tmpfs.total_gb)
} else { } else {
"— —/—".to_string() "— —/—".to_string()
}; };
let mut tmpfs_spans = vec![ let mut tmpfs_spans = vec![
Span::styled(tree_symbol, Typography::tree()), Span::styled(tree_symbol, Typography::tree()),
]; ];
@@ -774,6 +769,16 @@ impl SystemWidget {
lines.push(Line::from(tmpfs_spans)); lines.push(Line::from(tmpfs_spans));
} }
// Network section
if !self.network_interfaces.is_empty() {
lines.push(Line::from(vec![
Span::styled("Network:", Typography::widget_title())
]));
let network_lines = self.render_network();
lines.extend(network_lines);
}
// Storage section // Storage section
lines.push(Line::from(vec![ lines.push(Line::from(vec![
Span::styled("Storage:", Typography::widget_title()) Span::styled("Storage:", Typography::widget_title())

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "cm-dashboard-shared" name = "cm-dashboard-shared"
version = "0.1.165" version = "0.1.168"
edition = "2021" edition = "2021"
[dependencies] [dependencies]