v0.7.2: UI polish — list search, unified Hosts form, NIC link details, About refresh, spacing
Storage: - Filter inputs for the Available images table (matches filename, detected family, category, source) and the Unattended files list (name, kind). Pure client-side; shown when there's more than one entry. Forty-image libraries are now navigable. Hosts — one form, one mental model: - The separate Boot rules card is gone. The Pin form gains 'Architecture (optional)' next to Label (plus the v0.7.1 boot-binary pin): a full MAC with no architecture saves a per-host pin exactly as before; a MAC prefix and/or architecture saves a first-match-wins group rule. Saved rules render as a compact read-only 'Group rules' card with remove buttons. - The boot-decision webhook keeps working via /api/boot-rules but no longer has a UI knob (operator feedback: not needed in the UI). - Per-machine auto-deploy fields are rejected on group rules with a clear message (they're per-host values). Network: - New 'Link' row under NIC name: operstate · speed · duplex · port MAC, read from sysfs at startup (detect_link_info). Empty-degrades on non-Linux dev builds and virtual NICs. Confirms WHICH physical port answers PXE in multi-NIC/trunked environments. About: - Hero copy rewritten: positioning lead, three-pillar feature grid (Boot anything / Adapt to every machine / Run it in production), and the privacy + no-test-cert principles restated crisply. Spacing: - label.field:has(+ button) collapse fixes the doubled 30px gap above Queue 'Launch for all waiting' and Network 'Save' (now the same 16px as every other card action). Validation: clippy clean, fmt clean, 299 workspace tests green, webui syntax-checked. No protocol or boot-path changes in this release. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
29040e8a5a
commit
a71057fce6
@@ -215,6 +215,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
started_at: time::OffsetDateTime::now_utc(),
|
||||
public_base_url: public_base_url.clone(),
|
||||
nic_name: net.nic_name,
|
||||
nic_link: net.nic_link,
|
||||
subnet_mask: net.subnet_mask,
|
||||
gateway: net.gateway,
|
||||
};
|
||||
@@ -448,6 +449,12 @@ struct NetworkInfo {
|
||||
nic_name: String,
|
||||
subnet_mask: String,
|
||||
gateway: String,
|
||||
/// v0.7.2: physical link summary for the Network tab — operstate,
|
||||
/// negotiated speed/duplex, and the port's own MAC. Helps operators
|
||||
/// in multi-NIC / trunked environments confirm *which* port the PXE
|
||||
/// server actually answers on. Empty when sysfs isn't available
|
||||
/// (non-Linux dev builds) or the NIC wasn't identified.
|
||||
nic_link: String,
|
||||
}
|
||||
|
||||
/// Best-effort population of the Network tab's read-only fields. We shell
|
||||
@@ -508,9 +515,44 @@ fn detect_network_info(our_ip: Ipv4Addr) -> NetworkInfo {
|
||||
}
|
||||
}
|
||||
|
||||
info.nic_link = detect_link_info(&info.nic_name);
|
||||
|
||||
info
|
||||
}
|
||||
|
||||
/// v0.7.2: read the NIC's physical link details from sysfs. Every field
|
||||
/// is optional — virtual NICs report no speed (`-1` or absent), and
|
||||
/// non-Linux dev machines have no `/sys/class/net` at all — so the
|
||||
/// result is whatever could be read, joined human-readably, or empty.
|
||||
fn detect_link_info(nic: &str) -> String {
|
||||
if nic.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
let read = |file: &str| {
|
||||
std::fs::read_to_string(format!("/sys/class/net/{nic}/{file}"))
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
let mut parts: Vec<String> = Vec::new();
|
||||
let state = read("operstate");
|
||||
if !state.is_empty() {
|
||||
parts.push(format!("link {state}"));
|
||||
}
|
||||
let speed = read("speed");
|
||||
if !speed.is_empty() && speed != "-1" {
|
||||
parts.push(format!("{speed} Mb/s"));
|
||||
}
|
||||
let duplex = read("duplex");
|
||||
if !duplex.is_empty() && duplex != "unknown" {
|
||||
parts.push(format!("{duplex} duplex"));
|
||||
}
|
||||
let mac = read("address");
|
||||
if !mac.is_empty() {
|
||||
parts.push(format!("port {mac}"));
|
||||
}
|
||||
parts.join(" · ")
|
||||
}
|
||||
|
||||
fn prefix_to_dotted(prefix: u8) -> String {
|
||||
let prefix = prefix.min(32);
|
||||
let mask: u32 = if prefix == 0 {
|
||||
|
||||
Reference in New Issue
Block a user