Include WiFi and network interfaces to device details
build / build-linux-amd64 (push) Successful in 2m1s
build / build-linux-amd64 (push) Successful in 2m1s
This commit is contained in:
@@ -548,6 +548,21 @@ fn render_inventory_table(inv: &serde_json::Value) -> String {
|
||||
)
|
||||
};
|
||||
|
||||
let nics_html = render_nics(inv.get("network_interfaces"));
|
||||
let wifi_html = render_wifi(inv.get("wifi_current"), inv.get("wifi_nearby"));
|
||||
let public_ip_raw = inv
|
||||
.get("public_ip")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
let public_ip_html = if public_ip_raw.is_empty() {
|
||||
r##"<span class="text-slate-500">— (lookup failed or blocked)</span>"##.to_string()
|
||||
} else {
|
||||
format!(
|
||||
r##"<code class="font-mono text-xs text-sky-300 bg-slate-950 px-2 py-1 rounded border border-slate-800 select-all">{}</code>"##,
|
||||
html_escape(public_ip_raw)
|
||||
)
|
||||
};
|
||||
|
||||
format!(
|
||||
r##"<div class="space-y-4">
|
||||
<div class="rounded-md border border-slate-800 bg-slate-900 overflow-hidden">
|
||||
@@ -573,6 +588,15 @@ fn render_inventory_table(inv: &serde_json::Value) -> String {
|
||||
{disks}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-xs uppercase text-slate-500 mb-1">Network interfaces</h4>
|
||||
{nics}
|
||||
</div>
|
||||
{wifi}
|
||||
<div>
|
||||
<h4 class="text-xs uppercase text-slate-500 mb-1">Public IP (egress, last lookup)</h4>
|
||||
{public_ip}
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-xs uppercase text-slate-500 mb-1">BitLocker recovery key (system drive)</h4>
|
||||
{bl}
|
||||
@@ -590,10 +614,173 @@ fn render_inventory_table(inv: &serde_json::Value) -> String {
|
||||
cpu_lc = row("CPU logical cores", "cpu_cores_logical"),
|
||||
ram = row("RAM (GB)", "ram_gb"),
|
||||
disks = disks_html,
|
||||
nics = nics_html,
|
||||
wifi = wifi_html,
|
||||
public_ip = public_ip_html,
|
||||
bl = bl_html,
|
||||
)
|
||||
}
|
||||
|
||||
/// Render the network-interfaces array as a table (one row per NIC).
|
||||
/// Wi-Fi NICs get a small badge so the operator can spot them at a glance
|
||||
/// next to the Wi-Fi-current section. Empty input → dash.
|
||||
fn render_nics(nics: Option<&serde_json::Value>) -> String {
|
||||
let arr = match nics {
|
||||
Some(serde_json::Value::Array(a)) if !a.is_empty() => a,
|
||||
_ => {
|
||||
return r##"<div class="rounded-md border border-slate-800 bg-slate-900 p-2"><span class="text-slate-500">—</span></div>"##.to_string();
|
||||
}
|
||||
};
|
||||
let mut s = String::from(
|
||||
r##"<div class="rounded-md border border-slate-800 bg-slate-900 overflow-hidden p-2"><table class="w-full text-xs"><thead><tr class="text-slate-500"><th class="text-left font-medium px-2 py-1">Name</th><th class="text-left font-medium px-2 py-1">Description</th><th class="text-left font-medium px-2 py-1">MAC</th><th class="text-left font-medium px-2 py-1">Status</th><th class="text-left font-medium px-2 py-1">IPv4</th><th class="text-left font-medium px-2 py-1">IPv6</th><th class="text-right font-medium px-2 py-1">Mbps</th></tr></thead><tbody>"##,
|
||||
);
|
||||
for nic in arr {
|
||||
let name = fmt_inv_value(nic.get("name"));
|
||||
let descr = fmt_inv_value(nic.get("description"));
|
||||
let mac = fmt_inv_value(nic.get("mac"));
|
||||
let status = fmt_inv_value(nic.get("status"));
|
||||
let speed = fmt_inv_value(nic.get("speed_mbps"));
|
||||
let is_wifi = nic
|
||||
.get("is_wifi")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
let wifi_badge = if is_wifi {
|
||||
r##" <span class="ml-1 inline-block text-[10px] px-1 py-0 rounded bg-sky-900/50 text-sky-300">Wi-Fi</span>"##
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let ipv4 = render_ip_list(nic.get("ipv4"));
|
||||
let ipv6 = render_ip_list(nic.get("ipv6"));
|
||||
let _ = write!(
|
||||
&mut s,
|
||||
r##"<tr class="border-t border-slate-800 align-top"><td class="px-2 py-1 text-slate-200">{name}{badge}</td><td class="px-2 py-1 text-slate-400">{descr}</td><td class="px-2 py-1 font-mono text-slate-300">{mac}</td><td class="px-2 py-1 text-slate-400">{status}</td><td class="px-2 py-1 font-mono text-slate-300">{ipv4}</td><td class="px-2 py-1 font-mono text-slate-300 break-all">{ipv6}</td><td class="px-2 py-1 text-right font-mono text-slate-200">{speed}</td></tr>"##,
|
||||
name = name,
|
||||
badge = wifi_badge,
|
||||
descr = descr,
|
||||
mac = mac,
|
||||
status = status,
|
||||
ipv4 = ipv4,
|
||||
ipv6 = ipv6,
|
||||
speed = speed,
|
||||
);
|
||||
}
|
||||
s.push_str("</tbody></table></div>");
|
||||
s
|
||||
}
|
||||
|
||||
/// Render an array-of-strings IP list as a `<br>`-separated block, or
|
||||
/// dash when the array is empty / missing. Each address is HTML-escaped.
|
||||
fn render_ip_list(v: Option<&serde_json::Value>) -> String {
|
||||
match v {
|
||||
Some(serde_json::Value::Array(a)) if !a.is_empty() => a
|
||||
.iter()
|
||||
.filter_map(|x| x.as_str())
|
||||
.map(html_escape)
|
||||
.collect::<Vec<_>>()
|
||||
.join("<br>"),
|
||||
_ => "—".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the Wi-Fi section: current connection (if any) and the
|
||||
/// nearby-SSID scan (collapsed by default to keep the page short).
|
||||
/// Returns an empty string when neither is present, so the surrounding
|
||||
/// detail page can omit the heading entirely.
|
||||
fn render_wifi(
|
||||
current: Option<&serde_json::Value>,
|
||||
nearby: Option<&serde_json::Value>,
|
||||
) -> String {
|
||||
let has_current = matches!(current, Some(v) if v.is_object());
|
||||
let nearby_arr = match nearby {
|
||||
Some(serde_json::Value::Array(a)) if !a.is_empty() => Some(a),
|
||||
_ => None,
|
||||
};
|
||||
if !has_current && nearby_arr.is_none() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let current_html = if let Some(c) = current.filter(|v| v.is_object()) {
|
||||
// Rate: native API returns rx/tx in Kbps; render the higher of
|
||||
// the two as Mbps. Most APs report identical rx/tx, so a single
|
||||
// figure is plenty for the operator's "is this slow" question.
|
||||
let rate_mbps = {
|
||||
let rx = c.get("rx_kbps").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||
let tx = c.get("tx_kbps").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||
let max = rx.max(tx);
|
||||
if max == 0 {
|
||||
"—".to_string()
|
||||
} else {
|
||||
format!("{} Mbps", max / 1000)
|
||||
}
|
||||
};
|
||||
let signal_with_dbm = {
|
||||
let pct = c.get("signal_pct").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||
let dbm = c.get("rssi_dbm").and_then(|v| v.as_i64());
|
||||
match dbm {
|
||||
Some(d) => format!("{pct}% ({d} dBm)"),
|
||||
None => format!("{pct}%"),
|
||||
}
|
||||
};
|
||||
format!(
|
||||
r##"<div class="rounded-md border border-slate-800 bg-slate-900 p-3">
|
||||
<dl class="grid grid-cols-2 gap-x-6 gap-y-1 text-xs md:grid-cols-3">
|
||||
<div><dt class="text-slate-500">SSID</dt><dd class="text-slate-200 font-mono">{ssid}</dd></div>
|
||||
<div><dt class="text-slate-500">BSSID</dt><dd class="text-slate-300 font-mono">{bssid}</dd></div>
|
||||
<div><dt class="text-slate-500">Signal</dt><dd class="text-slate-200">{sig}</dd></div>
|
||||
<div><dt class="text-slate-500">Authentication</dt><dd class="text-slate-300">{auth}</dd></div>
|
||||
<div><dt class="text-slate-500">Cipher</dt><dd class="text-slate-300">{cipher}</dd></div>
|
||||
<div><dt class="text-slate-500">Rate</dt><dd class="text-slate-300">{rate}</dd></div>
|
||||
</dl>
|
||||
</div>"##,
|
||||
ssid = fmt_inv_value(c.get("ssid")),
|
||||
bssid = fmt_inv_value(c.get("bssid")),
|
||||
sig = signal_with_dbm,
|
||||
auth = fmt_inv_value(c.get("auth")),
|
||||
cipher = fmt_inv_value(c.get("cipher")),
|
||||
rate = rate_mbps,
|
||||
)
|
||||
} else {
|
||||
r##"<div class="rounded-md border border-slate-800 bg-slate-900 p-3 text-xs text-slate-500">Not connected to a Wi-Fi network.</div>"##.to_string()
|
||||
};
|
||||
|
||||
let nearby_html = if let Some(arr) = nearby_arr {
|
||||
let mut s = String::from(
|
||||
r##"<details class="rounded-md border border-slate-800 bg-slate-900">
|
||||
<summary class="cursor-pointer px-3 py-2 text-xs text-slate-400 hover:text-slate-200 select-none">Nearby SSIDs ({n})</summary>
|
||||
<div class="p-2"><table class="w-full text-xs"><thead><tr class="text-slate-500"><th class="text-left font-medium px-2 py-1">SSID</th><th class="text-left font-medium px-2 py-1">Authentication</th><th class="text-left font-medium px-2 py-1">Cipher</th><th class="text-right font-medium px-2 py-1">Signal</th></tr></thead><tbody>"##,
|
||||
);
|
||||
s = s.replace("{n}", &arr.len().to_string());
|
||||
for net in arr {
|
||||
let _ = write!(
|
||||
&mut s,
|
||||
r##"<tr class="border-t border-slate-800"><td class="px-2 py-1 font-mono text-slate-300">{ssid}</td><td class="px-2 py-1 text-slate-400">{auth}</td><td class="px-2 py-1 text-slate-400">{cipher}</td><td class="px-2 py-1 text-right font-mono text-slate-200">{sig}%</td></tr>"##,
|
||||
ssid = fmt_inv_value(net.get("ssid")),
|
||||
auth = fmt_inv_value(net.get("auth")),
|
||||
cipher = fmt_inv_value(net.get("cipher")),
|
||||
sig = fmt_inv_value(net.get("signal_pct")),
|
||||
);
|
||||
}
|
||||
s.push_str("</tbody></table></div></details>");
|
||||
s
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
format!(
|
||||
r##"<div>
|
||||
<h4 class="text-xs uppercase text-slate-500 mb-1">Wi-Fi (current connection)</h4>
|
||||
{current}
|
||||
{nearby}
|
||||
</div>"##,
|
||||
current = current_html,
|
||||
nearby = if nearby_html.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("<div class=\"mt-2\">{}</div>", nearby_html)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Render an elapsed-seconds count as a short "Xs / Xm / Xh / Xd" string
|
||||
/// for the offline tooltip. The exact heartbeat timestamp is already
|
||||
/// shown in the table cell — this is just for the friendly tooltip.
|
||||
|
||||
Reference in New Issue
Block a user