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
```
Network:
● eno1:
├─ ip: 192.168.30.105
└─ tailscale0: 100.125.108.16
● eno2:
└─ ip: 192.168.32.105
CPU:
● Load: 0.23 0.21 0.13
└─ Freq: 1048 MHz

6
Cargo.lock generated
View File

@@ -279,7 +279,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
[[package]]
name = "cm-dashboard"
version = "0.1.165"
version = "0.1.169"
dependencies = [
"anyhow",
"chrono",
@@ -301,7 +301,7 @@ dependencies = [
[[package]]
name = "cm-dashboard-agent"
version = "0.1.165"
version = "0.1.169"
dependencies = [
"anyhow",
"async-trait",
@@ -324,7 +324,7 @@ dependencies = [
[[package]]
name = "cm-dashboard-shared"
version = "0.1.165"
version = "0.1.169"
dependencies = [
"chrono",
"serde",

View File

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

View File

@@ -62,11 +62,19 @@ impl NetworkCollector {
for iface in ifaces {
let name = iface["ifname"].as_str().unwrap_or("").to_string();
// Skip loopback and empty names
if name.is_empty() || name == "lo" {
// Skip loopback, empty names, and ifb* interfaces
if name.is_empty() || name == "lo" || name.starts_with("ifb") {
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 ipv6_addresses = Vec::new();
@@ -91,7 +99,14 @@ impl NetworkCollector {
}
// 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 {
Self::get_link_status(&name)
} else {
@@ -99,12 +114,12 @@ impl NetworkCollector {
};
interfaces.push(NetworkInterfaceData {
name,
name: interface_name,
ipv4_addresses,
ipv6_addresses,
is_physical,
link_status,
parent_interface: None, // TODO: Implement virtual interface parent detection
parent_interface,
});
}
}

View File

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

View File

@@ -628,60 +628,108 @@ impl SystemWidget {
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();
// Render physical interfaces first
for (i, interface) in physical.iter().enumerate() {
let is_last = i == physical.len() - 1 && virtual_interfaces.is_empty();
let tree_symbol = if is_last { " └─ " } else { " ├─ " };
// Find standalone virtual interfaces (those without a parent)
let standalone_virtual: Vec<_> = virtual_interfaces.iter()
.filter(|i| i.parent_interface.is_none())
.collect();
// Show interface name with IPs
let mut interface_text = format!("{}: ", interface.name);
// Render physical interfaces with their children
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
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(
// Physical interface header with status icon
let mut header_spans = vec![];
header_spans.extend(StatusIcons::create_status_spans(
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
for (i, interface) in virtual_interfaces.iter().enumerate() {
let is_last = i == virtual_interfaces.len() - 1;
// Render standalone virtual interfaces (those without a parent)
for (virt_idx, interface) in standalone_virtual.iter().enumerate() {
let is_last = virt_idx == standalone_virtual.len() - 1;
let tree_symbol = if is_last { " └─ " } else { " ├─ " };
// Show interface name with IPs
let mut interface_text = format!("{}: ", interface.name);
// Virtual interface with IPs
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
if !interface.ipv4_addresses.is_empty() {
interface_text.push_str(&Self::compress_ipv4_addresses(&interface.ipv4_addresses));
}
let interface_text = if !ip_text.is_empty() {
format!("{}: {}", interface.name, ip_text)
} 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![
Span::styled(tree_symbol, Typography::tree()),
Span::styled(interface_text, Typography::secondary()),
@@ -710,28 +758,18 @@ impl SystemWidget {
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
lines.push(Line::from(vec![
Span::styled("CPU:", Typography::widget_title())
]));
let load_text = self.format_cpu_load();
let cpu_spans = StatusIcons::create_status_spans(
self.cpu_status.clone(),
&format!("Load: {}", load_text)
);
lines.push(Line::from(cpu_spans));
let freq_text = self.format_cpu_frequency();
lines.push(Line::from(vec![
Span::styled(" └─ ", Typography::tree()),
@@ -742,7 +780,7 @@ impl SystemWidget {
lines.push(Line::from(vec![
Span::styled("RAM:", Typography::widget_title())
]));
let memory_text = self.format_memory_usage();
let memory_spans = StatusIcons::create_status_spans(
self.memory_status.clone(),
@@ -754,16 +792,16 @@ impl SystemWidget {
for (i, tmpfs) in self.tmpfs_mounts.iter().enumerate() {
let is_last = i == self.tmpfs_mounts.len() - 1;
let tree_symbol = if is_last { " └─ " } else { " ├─ " };
let usage_text = if tmpfs.total_gb > 0.0 {
format!("{:.0}% {:.1}GB/{:.1}GB",
tmpfs.usage_percent,
tmpfs.used_gb,
format!("{:.0}% {:.1}GB/{:.1}GB",
tmpfs.usage_percent,
tmpfs.used_gb,
tmpfs.total_gb)
} else {
"— —/—".to_string()
};
let mut tmpfs_spans = vec![
Span::styled(tree_symbol, Typography::tree()),
];
@@ -774,6 +812,16 @@ impl SystemWidget {
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
lines.push(Line::from(vec![
Span::styled("Storage:", Typography::widget_title())

View File

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