Compare commits

...

4 Commits

Author SHA1 Message Date
8aefab83ae Fix network interface display for VLANs and physical NICs
All checks were successful
Build and Release / build-and-release (push) Successful in 1m11s
Agent changes:
- Filter out ifb* interfaces from network display
- Parse @parent notation for VLAN interfaces (e.g., lan@enp0s31f6)
- Show physical interfaces even without IP addresses
- Only filter virtual interfaces that have no IPs
- Extract parent interface relationships for proper nesting

Dashboard changes:
- Nest VLAN/child interfaces under their physical parent
- Show physical NICs with status icons even when down
- Display child interfaces grouped under parent interface
- Keep standalone virtual interfaces at root level

Updated to version 0.1.169
2025-11-26 23:47:16 +01:00
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 142 additions and 73 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.169"
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.169"
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.169"
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.169"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

View File

@@ -62,11 +62,19 @@ impl NetworkCollector {
for iface in ifaces { for iface in ifaces {
let name = iface["ifname"].as_str().unwrap_or("").to_string(); let name = iface["ifname"].as_str().unwrap_or("").to_string();
// Skip loopback and empty names // Skip loopback, empty names, and ifb* interfaces
if name.is_empty() || name == "lo" { if name.is_empty() || name == "lo" || name.starts_with("ifb") {
continue; continue;
} }
// Parse parent interface from @parent notation (e.g., lan@enp0s31f6)
let (interface_name, parent_interface) = if let Some(at_pos) = name.find('@') {
let (child, parent) = name.split_at(at_pos);
(child.to_string(), Some(parent[1..].to_string()))
} else {
(name.clone(), None)
};
let mut ipv4_addresses = Vec::new(); let mut ipv4_addresses = Vec::new();
let mut ipv6_addresses = Vec::new(); let mut ipv6_addresses = Vec::new();
@@ -91,7 +99,14 @@ impl NetworkCollector {
} }
// 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(&interface_name);
// Only filter out virtual interfaces without IPs
// Physical interfaces should always be shown even if down/no IPs
if !is_physical && ipv4_addresses.is_empty() && ipv6_addresses.is_empty() {
continue;
}
let link_status = if is_physical { let link_status = if is_physical {
Self::get_link_status(&name) Self::get_link_status(&name)
} else { } else {
@@ -99,12 +114,12 @@ impl NetworkCollector {
}; };
interfaces.push(NetworkInterfaceData { interfaces.push(NetworkInterfaceData {
name, name: interface_name,
ipv4_addresses, ipv4_addresses,
ipv6_addresses, ipv6_addresses,
is_physical, is_physical,
link_status, link_status,
parent_interface: None, // TODO: Implement virtual interface parent detection parent_interface,
}); });
} }
} }

View File

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

View File

@@ -628,60 +628,108 @@ 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 // Find standalone virtual interfaces (those without a parent)
for (i, interface) in physical.iter().enumerate() { let standalone_virtual: Vec<_> = virtual_interfaces.iter()
let is_last = i == physical.len() - 1 && virtual_interfaces.is_empty(); .filter(|i| i.parent_interface.is_none())
let tree_symbol = if is_last { " └─ " } else { " ├─ " }; .collect();
// Show interface name with IPs // Render physical interfaces with their children
let mut interface_text = format!("{}: ", interface.name); for (phy_idx, interface) in physical.iter().enumerate() {
let is_last_physical = phy_idx == physical.len() - 1 && standalone_virtual.is_empty();
// Add compressed IPv4 addresses // Physical interface header with status icon
if !interface.ipv4_addresses.is_empty() { let mut header_spans = vec![];
interface_text.push_str(&Self::compress_ipv4_addresses(&interface.ipv4_addresses)); header_spans.extend(StatusIcons::create_status_spans(
}
// 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));
// Find child interfaces for this physical interface
let children: Vec<_> = virtual_interfaces.iter()
.filter(|vi| {
if let Some(parent) = &vi.parent_interface {
parent == &interface.name
} else {
false
}
})
.collect();
// Count total items under this physical interface (IPs + children)
let ip_count = interface.ipv4_addresses.len() + interface.ipv6_addresses.len();
let total_children = ip_count + children.len();
let mut child_index = 0;
// IPv4 addresses on the physical interface itself
for ipv4 in &interface.ipv4_addresses {
child_index += 1;
let is_last = child_index == total_children && is_last_physical;
let tree_symbol = if is_last { " └─ " } else { " ├─ " };
lines.push(Line::from(vec![
Span::styled(tree_symbol, Typography::tree()),
Span::styled(format!("ip: {}", ipv4), Typography::secondary()),
]));
}
// IPv6 addresses on the physical interface itself
for ipv6 in &interface.ipv6_addresses {
child_index += 1;
let is_last = child_index == total_children && is_last_physical;
let tree_symbol = if is_last { " └─ " } else { " ├─ " };
lines.push(Line::from(vec![
Span::styled(tree_symbol, Typography::tree()),
Span::styled(format!("ip: {}", ipv6), Typography::secondary()),
]));
}
// Child virtual interfaces (VLANs, etc.)
for child in children {
child_index += 1;
let is_last = child_index == total_children && is_last_physical;
let tree_symbol = if is_last { " └─ " } else { " ├─ " };
let ip_text = if !child.ipv4_addresses.is_empty() {
Self::compress_ipv4_addresses(&child.ipv4_addresses)
} else if !child.ipv6_addresses.is_empty() {
child.ipv6_addresses.join(", ")
} else {
String::new()
};
let child_text = if !ip_text.is_empty() {
format!("{}: {}", child.name, ip_text)
} else {
format!("{}:", child.name)
};
lines.push(Line::from(vec![
Span::styled(tree_symbol, Typography::tree()),
Span::styled(child_text, 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 standalone_virtual.iter().enumerate() {
let is_last = i == virtual_interfaces.len() - 1; let is_last = virt_idx == standalone_virtual.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,16 +758,6 @@ 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())
@@ -774,6 +812,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.169"
edition = "2021" edition = "2021"
[dependencies] [dependencies]