use crate::config::Config;
use crate::diagnostics::{DiagnosticResults, DiagnosticStatus, TechnicianResults};
use crate::render::color::{colorize_status, dim};
use crate::render::table::ReportBuilder;
pub fn render(results: &DiagnosticResults, config: &Config) -> String {
let label_width = 16;
let data_width = 40;
let chars = config.box_chars();
let mut output = String::new();
let mut builder = ReportBuilder::new(label_width, data_width, chars)
.header(config.title(), config.subtitle());
builder = builder.span_row(" DIAGNOSTIC SUMMARY").divider();
builder = render_summary_row(builder, &results.adapters, config);
builder = render_summary_row(builder, &results.interfaces, config);
builder = render_summary_row(builder, &results.gateway, config);
builder = render_summary_row(builder, &results.dns, config);
builder = render_summary_row(builder, &results.public_ip, config);
builder = render_summary_row(builder, &results.latency, config);
builder = render_summary_row(builder, &results.speed, config);
builder = render_summary_row(builder, &results.ports, config);
let (fail_count, warn_count) = count_issues(results);
let overall = format_overall(fail_count, warn_count, config);
builder = builder.divider();
builder = builder.span_row(&format!(" OVERALL: {}", overall));
if fail_count > 0 {
builder = builder.span_row(&dim(" Run 'nd300 -f' to attempt automatic fixes", config));
}
output.push_str(&builder.finish());
output.push('\n');
if let Some(ref ifaces) = results.interface_details {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b
.full_top_border()
.span_row(" NETWORK INTERFACES")
.divider();
for (i, iface) in ifaces.iter().enumerate() {
if i > 0 {
b = b.divider();
}
b = b.row("Name", &iface.name);
b = b.row("Type", &iface.interface_type);
b = b.row("MAC", &iface.mac);
b = b.row("Status", if iface.is_up { "Up" } else { "Down" });
for ip in &iface.ip_addresses {
b = b.row("IP Address", ip);
}
}
output.push_str(&b.finish());
output.push('\n');
}
if let Some(ref adapters) = results.adapter_details {
if !adapters.is_empty() {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b.full_top_border().span_row(" NETWORK ADAPTERS").divider();
for (i, adapter) in adapters.iter().enumerate() {
if i > 0 {
b = b.divider();
}
let adapter_label = adapter.description.as_deref().unwrap_or(&adapter.name);
b = b.row("Adapter", adapter_label);
let type_detail = if let Some(ref pm) = adapter.physical_medium {
format!("{} ({})", adapter.adapter_type, pm)
} else {
adapter.adapter_type.clone()
};
b = b.row("Type", &type_detail);
b = b.row("Status", &adapter.status);
if let Some(ref mac) = adapter.mac_address {
b = b.row("MAC", mac);
}
match (adapter.link_speed_mbps, adapter.rx_link_speed_mbps) {
(Some(tx), Some(rx)) if tx == rx => {
b = b.row("Link Speed", &format_link_speed(tx));
}
(Some(tx), Some(rx)) => {
b = b.row(
"Link Speed",
&format!(
"TX: {} / RX: {}",
format_link_speed(tx),
format_link_speed(rx)
),
);
}
(Some(tx), None) => {
b = b.row("Link Speed", &format_link_speed(tx));
}
_ => {}
}
if let Some(ref gws) = adapter.gateways {
for gw in gws {
b = b.row("Gateway", gw);
}
}
if let Some(ref dns) = adapter.dns_servers {
for server in dns {
b = b.row("DNS", server);
}
}
if let Some(mtu) = adapter.mtu {
b = b.row("MTU", &mtu.to_string());
}
if let Some(metric) = adapter.ipv4_metric {
b = b.row("IPv4 Metric", &metric.to_string());
}
if let Some(ref drv) = adapter.driver_name {
b = b.row("Driver", drv);
}
if let Some(ref ver) = adapter.driver_version {
b = b.row("Driver Version", ver);
}
if let Some(ref date) = adapter.driver_date {
b = b.row("Driver Date", date);
}
}
output.push_str(&b.finish());
output.push('\n');
}
}
if let Some(ref gw) = results.gateway_details {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b.full_top_border().span_row(" GATEWAY").divider();
b = b.row("IP Address", &gw.ip);
b = b.row("Reachable", if gw.reachable { "Yes" } else { "No" });
if let Some(lat) = gw.latency_ms {
b = b.row("Latency", &format!("{:.1}ms", lat));
}
if gw.packets_sent > 0 {
b = b.row(
"Probes",
&format!("{}/{} replies", gw.packets_received, gw.packets_sent),
);
}
output.push_str(&b.finish());
output.push('\n');
}
if let Some(ref dns) = results.dns_details {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b.full_top_border().span_row(" DNS SERVERS").divider();
for server in &dns.servers {
b = b.row("Server", &server.address);
}
if dns.resolution_tests.is_empty() {
if let Some(ref test) = dns.resolution_test {
b = b.divider();
b = b.row("Test Domain", &test.domain);
b = b.row("Resolved", if test.resolved { "Yes" } else { "No" });
b = b.row("Time", &format!("{:.1}ms", test.resolution_time_ms));
for ip in &test.resolved_ips {
b = b.row("Resolved IP", ip);
}
}
} else {
for test in &dns.resolution_tests {
b = b.divider();
b = b.row("Test Domain", &test.domain);
b = b.row("Resolved", if test.resolved { "Yes" } else { "No" });
b = b.row("Time", &format!("{:.1}ms", test.resolution_time_ms));
for ip in &test.resolved_ips {
b = b.row("Resolved IP", ip);
}
}
}
output.push_str(&b.finish());
output.push('\n');
}
if let Some(ref pip) = results.public_ip_details {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b
.full_top_border()
.span_row(" PUBLIC IP & GEOLOCATION")
.divider();
b = b.row("Public IP", &pip.ip);
b = b.row("Lookup Time", &format!("{:.0}ms", pip.lookup_time_ms));
b = b.row("Behind NAT", if pip.behind_nat { "Yes" } else { "No" });
if let Some(ref city) = pip.city {
b = b.row("City", city);
}
if let Some(ref region) = pip.region {
b = b.row("Region", region);
}
if let Some(ref country) = pip.country {
b = b.row("Country", country);
}
if let Some(ref isp) = pip.isp {
b = b.row("ISP", isp);
}
if let Some(ref org) = pip.org {
b = b.row("Organization", org);
}
output.push_str(&b.finish());
output.push('\n');
}
if let Some(ref latencies) = results.latency_details {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b.full_top_border().span_row(" LATENCY TESTS").divider();
for (i, lat) in latencies.iter().enumerate() {
if i > 0 {
b = b.divider();
}
b = b.row("Host", &format!("{} ({})", lat.host, lat.label));
b = b.row("Reachable", if lat.reachable { "Yes" } else { "No" });
if let Some(min) = lat.min_ms {
b = b.row("Min", &format!("{:.1}ms", min));
}
if let Some(avg) = lat.avg_ms {
b = b.row("Avg", &format!("{:.1}ms", avg));
}
if let Some(max) = lat.max_ms {
b = b.row("Max", &format!("{:.1}ms", max));
}
if let Some(jitter) = lat.jitter_ms {
b = b.row("Jitter", &format!("{:.1}ms", jitter));
}
b = b.row("Packet Loss", &format!("{:.0}%", lat.packet_loss));
}
output.push_str(&b.finish());
output.push('\n');
}
if let Some(ref speed) = results.speed_details {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b.full_top_border().span_row(" SPEED TEST").divider();
if let Some(ping) = speed.ping_ms {
b = b.row("Ping", &format!("{:.1} ms", ping));
}
if let Some(jitter) = speed.jitter_ms {
b = b.row("Jitter", &format!("{:.1} ms", jitter));
}
let dl_margin = speed
.confidence_intervals
.as_ref()
.and_then(|ci| ci.download.as_ref())
.map(|ci| format!(" ±{}", crate::speedtest::format_mbps(ci.margin)))
.unwrap_or_default();
let ul_margin = speed
.confidence_intervals
.as_ref()
.and_then(|ci| ci.upload.as_ref())
.map(|ci| format!(" ±{}", crate::speedtest::format_mbps(ci.margin)))
.unwrap_or_default();
b = b.row(
"Download",
&format!(
"{}{} (avg)",
crate::speedtest::format_mbps(speed.download_mbps),
dl_margin
),
);
b = b.row(
"Upload",
&format!(
"{}{} (avg)",
crate::speedtest::format_mbps(speed.upload_mbps),
ul_margin
),
);
if let Some(loss) = speed.packet_loss_pct {
b = b.row("Packet Loss", &format!("{:.0}%", loss));
}
b = b.row("Duration", &format!("{:.1}s", speed.duration_s));
if !speed.merge_exclusions.is_empty() {
let list = speed
.merge_exclusions
.iter()
.map(|e| {
format!(
"{} {} ({} sample{})",
e.provider,
if e.direction == "download" {
"DL"
} else {
"UL"
},
e.samples,
if e.samples == 1 { "" } else { "s" },
)
})
.collect::<Vec<_>>()
.join(", ");
b = b.row("Excluded", &list);
}
for provider in &speed.providers {
if provider.error.is_some() {
continue;
}
b = b.section_header(&provider.provider);
b = b.row("Server", &provider.server);
if let Some(ref location) = provider.location {
b = b.row("Location", location);
}
if let Some(dl) = provider.download_mbps {
b = b.row("Download", &crate::speedtest::format_mbps(dl));
}
if let Some(ul) = provider.upload_mbps {
b = b.row("Upload", &crate::speedtest::format_mbps(ul));
}
b = b.row("DL Data", &format_bytes(provider.download_bytes));
b = b.row("UL Data", &format_bytes(provider.upload_bytes));
}
output.push_str(&b.finish());
output.push('\n');
}
if let Some(ref ports) = results.port_details {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b
.full_top_border()
.span_row(" PORT CONNECTIVITY")
.divider();
for port in ports {
let status = match port.outcome {
crate::diagnostics::ports::PortOutcome::Open => "Open",
crate::diagnostics::ports::PortOutcome::Blocked => "Blocked",
crate::diagnostics::ports::PortOutcome::Unresolved => "Untested (DNS)",
};
let lat = port
.latency_ms
.map(|l| format!(" ({:.0}ms)", l))
.unwrap_or_default();
b = b.row(
&format!("{} ({})", port.service, port.port),
&format!("{}{}", status, lat),
);
}
output.push_str(&b.finish());
output.push('\n');
}
if let Some(ref tech) = results.technician {
output.push_str(&render_technician_details(tech, config));
}
output.push_str(&format!(" Report generated: {}\n", results.timestamp));
output.push('\n');
output
}
fn render_technician_details(tech: &TechnicianResults, config: &Config) -> String {
let label_width = 16;
let data_width = 40;
let chars = config.box_chars();
let mut output = String::new();
if let Some(ref arp) = tech.arp_table {
if !arp.is_empty() {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b.full_top_border().span_row(" ARP TABLE").divider();
for entry in arp.iter().take(30) {
b = b.row(&entry.ip, &format!("{} ({})", entry.mac, entry.entry_type));
}
if arp.len() > 30 {
b = b.row("", &format!("... and {} more", arp.len() - 30));
}
if let Some(ref health) = tech.arp_health {
b = b.divider();
b = b.row(
"Gateway in ARP",
if health.gateway_in_table { "Yes" } else { "No" },
);
if let Some(ref mac) = health.gateway_mac {
b = b.row("Gateway MAC", mac);
}
for dup in &health.duplicate_ip_macs {
b = b.row(&format!("Duplicate {}", dup.ip), &dup.macs.join(", "));
}
b = b.row("Assessment", &health.assessment);
}
output.push_str(&b.finish());
output.push('\n');
}
}
if let Some(ref routes) = tech.routing_table {
if !routes.is_empty() {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b.full_top_border().span_row(" ROUTING TABLE").divider();
for route in routes.iter().take(20) {
let gw = if route.gateway.is_empty() {
"direct".to_string()
} else {
route.gateway.clone()
};
let metric = route
.metric
.map(|m| format!(" m:{}", m))
.unwrap_or_default();
b = b.row(
&route.destination,
&format!("via {} dev {}{}", gw, route.interface, metric),
);
}
if routes.len() > 20 {
b = b.row("", &format!("... and {} more", routes.len() - 20));
}
output.push_str(&b.finish());
output.push('\n');
}
}
if let Some(ref conns) = tech.active_connections {
if !conns.is_empty() {
let established: Vec<_> = conns
.iter()
.filter(|c| c.state == "ESTABLISHED")
.take(20)
.collect();
if !established.is_empty() {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b
.full_top_border()
.span_row(" ACTIVE CONNECTIONS (ESTABLISHED)")
.divider();
for conn in &established {
let proc_info = conn.process_name.as_deref().unwrap_or("?");
b = b.row(
&conn.local_addr,
&format!("{} [{}]", conn.remote_addr, proc_info),
);
}
let total_established = conns.iter().filter(|c| c.state == "ESTABLISHED").count();
if total_established > 20 {
b = b.row("", &format!("... and {} more", total_established - 20));
}
output.push_str(&b.finish());
output.push('\n');
}
}
}
if let Some(ref ports) = tech.listening_ports {
if !ports.is_empty() {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b.full_top_border().span_row(" LISTENING PORTS").divider();
for port in ports.iter().take(20) {
let proc_info = port.process_name.as_deref().unwrap_or("?");
b = b.row(
&format!("{} :{}", port.protocol, port.port),
&format!("{} [{}]", port.address, proc_info),
);
}
if ports.len() > 20 {
b = b.row("", &format!("... and {} more", ports.len() - 20));
}
output.push_str(&b.finish());
output.push('\n');
}
}
if let Some(ref dhcp) = tech.dhcp_info {
if !dhcp.is_empty() {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b.full_top_border().span_row(" DHCP LEASES").divider();
for (i, lease) in dhcp.iter().enumerate() {
if i > 0 {
b = b.divider();
}
b = b.row("Interface", &lease.interface);
b = b.row(
"DHCP Enabled",
if lease.dhcp_enabled { "Yes" } else { "No" },
);
if let Some(ref server) = lease.dhcp_server {
b = b.row("DHCP Server", server);
}
if let Some(ref ip) = lease.ip_address {
b = b.row("IP Address", ip);
}
if let Some(ref obtained) = lease.lease_obtained {
b = b.row("Obtained", obtained);
}
if let Some(ref expires) = lease.lease_expires {
b = b.row("Expires", expires);
}
}
output.push_str(&b.finish());
output.push('\n');
}
}
if let Some(ref stats) = tech.protocol_stats {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b
.full_top_border()
.span_row(" PROTOCOL STATISTICS")
.divider();
b = b.row("TCP Active Opens", &stats.tcp.active_opens.to_string());
b = b.row("TCP Passive Opens", &stats.tcp.passive_opens.to_string());
b = b.row("TCP Current", &stats.tcp.current_connections.to_string());
b = b.row("TCP Failed", &stats.tcp.failed_connections.to_string());
b = b.row("TCP Resets", &stats.tcp.reset_connections.to_string());
b = b.row(
"TCP Retransmits",
&stats.tcp.segments_retransmitted.to_string(),
);
b = b.row("TCP Segments In", &stats.tcp.segments_received.to_string());
b = b.row("TCP Segments Out", &stats.tcp.segments_sent.to_string());
b = b.divider();
b = b.row("UDP In", &stats.udp.datagrams_received.to_string());
b = b.row("UDP Out", &stats.udp.datagrams_sent.to_string());
b = b.row("UDP Errors", &stats.udp.receive_errors.to_string());
b = b.divider();
b = b.row("ICMP In", &stats.icmp.messages_received.to_string());
b = b.row("ICMP Out", &stats.icmp.messages_sent.to_string());
b = b.row("ICMP Errors In", &stats.icmp.errors_received.to_string());
output.push_str(&b.finish());
output.push('\n');
}
if let Some(ref hw) = tech.adapter_hw_stats {
if !hw.is_empty() {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b
.full_top_border()
.span_row(" ADAPTER HARDWARE STATS")
.divider();
for (i, stat) in hw.iter().enumerate() {
if i > 0 {
b = b.divider();
}
b = b.row("Interface", &stat.name);
b = b.row("RX Bytes", &format_bytes(stat.rx_bytes));
b = b.row("TX Bytes", &format_bytes(stat.tx_bytes));
b = b.row("RX Packets", &stat.rx_packets.to_string());
b = b.row("TX Packets", &stat.tx_packets.to_string());
b = b.row("RX Errors", &stat.rx_errors.to_string());
b = b.row("TX Errors", &stat.tx_errors.to_string());
if let Some(ref speed) = stat.link_speed {
b = b.row("Link Speed", speed);
}
if let Some(ref dup) = stat.duplex {
b = b.row("Duplex", dup);
}
}
output.push_str(&b.finish());
output.push('\n');
}
}
if let Some(ref proxy) = tech.proxy_config {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b
.full_top_border()
.span_row(" PROXY CONFIGURATION")
.divider();
b = b.row(
"Proxy Enabled",
if proxy.proxy_enabled { "Yes" } else { "No" },
);
if let Some(ref http) = proxy.http_proxy {
b = b.row("HTTP Proxy", http);
}
if let Some(ref https) = proxy.https_proxy {
b = b.row("HTTPS Proxy", https);
}
if let Some(ref socks) = proxy.socks_proxy {
b = b.row("SOCKS Proxy", socks);
}
if let Some(ref pac) = proxy.pac_url {
b = b.row("PAC URL", pac);
}
if let Some(reachable) = proxy.pac_reachable {
let size = proxy
.pac_size_bytes
.map(|s| format!(" ({} bytes)", s))
.unwrap_or_default();
b = b.row(
"PAC Reachable",
&if reachable {
format!("Yes{}", size)
} else {
"NO — configured PAC script is unreachable".to_string()
},
);
}
if let Some(wpad) = proxy.wpad_dns_detected {
b = b.row(
"WPAD DNS",
if wpad {
"Detected — proxy auto-discovery is live on this network"
} else {
"Not detected"
},
);
}
if let Some(ref no) = proxy.no_proxy {
b = b.row("No Proxy", no);
}
output.push_str(&b.finish());
output.push('\n');
}
if let Some(ref vpns) = tech.vpn_info {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b.full_top_border().span_row(" VPN DETECTION").divider();
for (i, vpn) in vpns.iter().enumerate() {
if i > 0 {
b = b.divider();
}
b = b.row("VPN Adapter", &vpn.name);
b = b.row("Type", &vpn.adapter_type);
b = b.row("Status", &vpn.status);
if let Some(ref vendor) = vpn.vendor {
b = b.row("Vendor", vendor);
}
if vpn.is_enterprise {
b = b.row("Policy", "Enterprise/Managed");
}
if let Some(ref iface) = vpn.interface_name {
b = b.row("Interface", iface);
}
if let Some(ref ip) = vpn.ip_address {
b = b.row("IP Address", ip);
}
}
output.push_str(&b.finish());
output.push('\n');
}
if let Some(ref fw) = tech.firewall_info {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b.full_top_border().span_row(" FIREWALL STATUS").divider();
b = b.row("Status", &fw.summary);
for profile in &fw.profiles {
b = b.row(
&profile.name,
if profile.enabled {
"Enabled"
} else {
"Disabled"
},
);
}
output.push_str(&b.finish());
output.push('\n');
}
if let Some(ref cache) = tech.dns_cache {
if !cache.is_empty() {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b.full_top_border().span_row(" DNS CACHE").divider();
for entry in cache.iter().take(20) {
b = b.row(
&entry.name,
&format!("{}: {}", entry.record_type, entry.data),
);
}
if cache.len() > 20 {
b = b.row("", &format!("... and {} more entries", cache.len() - 20));
}
output.push_str(&b.finish());
output.push('\n');
}
}
if let Some(ref ipv6) = tech.ipv6_info {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b.full_top_border().span_row(" IPv6 STATUS").divider();
b = b.row("Available", if ipv6.available { "Yes" } else { "No" });
let conn_str = match ipv6.connectivity {
crate::diagnostics::ipv6::Ipv6Connectivity::Full => "Full",
crate::diagnostics::ipv6::Ipv6Connectivity::LinkLocal => "Link-local only",
crate::diagnostics::ipv6::Ipv6Connectivity::None => "None",
};
b = b.row("Connectivity", conn_str);
b = b.row("Dual Stack", if ipv6.dual_stack { "Yes" } else { "No" });
if let Some(http_ok) = ipv6.v6_http_ok {
b = b.row("HTTPS over v6", if http_ok { "Working" } else { "FAILED" });
}
if let (Some(v4), Some(v6)) = (ipv6.v4_connect_ms, ipv6.v6_connect_ms) {
b = b.row("v4 vs v6 connect", &format!("{:.0}ms vs {:.0}ms", v4, v6));
}
if let Some(penalty) = ipv6.v6_penalty_ms {
if penalty > 0.0 {
b = b.row("v6 penalty", &format!("+{:.0}ms", penalty));
}
}
for addr in ipv6.addresses.iter().take(10) {
b = b.row(
&addr.scope,
&format!("{} ({})", addr.address, addr.interface),
);
}
output.push_str(&b.finish());
output.push('\n');
}
if let Some(ref mtus) = tech.mtu_info {
if !mtus.is_empty() {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b
.full_top_border()
.span_row(" MTU PER INTERFACE")
.divider();
for mtu in mtus {
b = b.row(&mtu.interface, &mtu.mtu.to_string());
}
if let Some(ref pm) = tech.path_mtu {
b = b.divider();
b = b.row(
&format!("Path MTU ({})", pm.target),
&if pm.path_mtu > 0 {
format!("{} ({} probes)", pm.path_mtu, pm.probes)
} else {
"unmeasurable".to_string()
},
);
b = b.row("Assessment", &pm.assessment);
}
output.push_str(&b.finish());
output.push('\n');
}
}
if let Some(ref states) = tech.connection_states {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b
.full_top_border()
.span_row(" TCP CONNECTION STATES")
.divider();
b = b.row("ESTABLISHED", &states.established.to_string());
b = b.row("TIME_WAIT", &states.time_wait.to_string());
b = b.row("CLOSE_WAIT", &states.close_wait.to_string());
b = b.row("FIN_WAIT_1", &states.fin_wait_1.to_string());
b = b.row("FIN_WAIT_2", &states.fin_wait_2.to_string());
b = b.row("SYN_SENT", &states.syn_sent.to_string());
b = b.row("SYN_RECEIVED", &states.syn_received.to_string());
b = b.row("CLOSING", &states.closing.to_string());
b = b.row("LAST_ACK", &states.last_ack.to_string());
b = b.row("LISTEN", &states.listen.to_string());
output.push_str(&b.finish());
output.push('\n');
}
if let Some(ref bb) = tech.bufferbloat {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b.full_top_border().span_row(" BUFFERBLOAT TEST").divider();
b = b.row("Grade", &bb.grade);
b = b.row(
"Idle Latency",
&format!(
"{:.1}ms ({} probes)",
bb.unloaded_latency_ms, bb.samples_idle
),
);
if let Some(loaded) = bb.loaded_latency_ms {
let grade = bb
.download_grade
.as_deref()
.map(|g| format!(" — grade {}", g))
.unwrap_or_default();
b = b.row(
"Loaded (down)",
&format!(
"{:.1}ms{}{}",
loaded,
bb.download_bloat_ms
.map(|d| format!(" (+{:.1}ms)", d))
.unwrap_or_default(),
grade
),
);
}
if let Some(ul_loaded) = bb.upload_loaded_latency_ms {
let grade = bb
.upload_grade
.as_deref()
.map(|g| format!(" — grade {}", g))
.unwrap_or_default();
b = b.row(
"Loaded (up)",
&format!(
"{:.1}ms{}{}",
ul_loaded,
bb.upload_bloat_ms
.map(|d| format!(" (+{:.1}ms)", d))
.unwrap_or_default(),
grade
),
);
}
b = b.row("Assessment", &bb.description);
output.push_str(&b.finish());
output.push('\n');
}
if let Some(ref rdns) = tech.reverse_dns {
if !rdns.is_empty() {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b.full_top_border().span_row(" REVERSE DNS").divider();
for entry in rdns {
let hostname = entry.hostname.as_deref().unwrap_or("(no PTR)");
b = b.row(&format!("{} ({})", entry.label, entry.ip), hostname);
}
output.push_str(&b.finish());
output.push('\n');
}
}
if let Some(ref tls) = tech.tls_inspection {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b
.full_top_border()
.span_row(" TLS INSPECTION CHECK")
.divider();
b = b.row("Intercepted", if tls.detected { "DETECTED" } else { "No" });
b = b.row("Assessment", &tls.description);
output.push_str(&b.finish());
output.push('\n');
}
if let Some(ref traffic) = tech.traffic_counters {
if !traffic.is_empty() {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b
.full_top_border()
.span_row(" TRAFFIC COUNTERS (since boot)")
.divider();
for counter in traffic {
b = b.row(
&counter.interface,
&format!("RX {} / TX {}", counter.rx_formatted, counter.tx_formatted),
);
}
output.push_str(&b.finish());
output.push('\n');
}
}
if let Some(ref path) = tech.route_path {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b
.full_top_border()
.span_row(" ROUTE PATH (TRACEROUTE)")
.divider();
for hop in path.hops.iter().take(16) {
let value = if hop.timed_out {
"*".to_string()
} else {
format!(
"{}{}",
hop.ip.as_deref().unwrap_or("?"),
hop.avg_ms
.map(|ms| format!(" {:.1}ms", ms))
.unwrap_or_default()
)
};
b = b.row(&format!("hop {}", hop.number), &value);
}
b = b.divider();
b = b.row("Reached", if path.reached { "Yes" } else { "No" });
if let Some(first) = path.first_hop_ms {
b = b.row("First Hop", &format!("{:.1}ms (your router)", first));
}
if let Some(boundary) = path.isp_boundary_hop {
b = b.row("ISP Boundary", &format!("hop {}", boundary));
}
if let Some(ref jump) = path.largest_jump {
b = b.row(
"Largest Jump",
&format!(
"+{:.1}ms (hop {}→{}, {})",
jump.delta_ms, jump.from_hop, jump.to_hop, jump.segment
),
);
}
b = b.row("Assessment", &path.assessment);
output.push_str(&b.finish());
output.push('\n');
}
if let Some(ref losses) = tech.packet_loss {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b
.full_top_border()
.span_row(" SUSTAINED PACKET LOSS")
.divider();
for (i, loss) in losses.iter().enumerate() {
if i > 0 {
b = b.divider();
}
b = b.row("Target", &format!("{} ({})", loss.host, loss.label));
b = b.row(
"Loss",
&format!(
"{:.1}% ({}/{} received)",
loss.loss_pct, loss.received, loss.sent
),
);
if let (Some(min), Some(avg), Some(max)) = (loss.min_ms, loss.avg_ms, loss.max_ms) {
b = b.row(
"Latency",
&format!("min {:.1} / avg {:.1} / max {:.1}ms", min, avg, max),
);
}
if let Some(p95) = loss.p95_ms {
b = b.row("P95", &format!("{:.1}ms", p95));
}
if let Some(jitter) = loss.jitter_ms {
b = b.row("Jitter", &format!("{:.1}ms", jitter));
}
b = b.row("Assessment", &loss.assessment);
}
output.push_str(&b.finish());
output.push('\n');
}
if let Some(ref nat) = tech.nat_analysis {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b.full_top_border().span_row(" NAT ANALYSIS").divider();
if let Some(ref gw) = nat.gateway_ip {
b = b.row(
"Gateway",
&format!(
"{} ({})",
gw,
if nat.gateway_is_private {
"private"
} else {
"public"
}
),
);
}
if let Some(ref hop2) = nat.hop2_ip {
b = b.row(
"Second Hop",
&format!(
"{} ({})",
hop2,
if nat.hop2_is_private {
"private"
} else {
"public"
}
),
);
}
if let Some(ref pip) = nat.public_ip {
b = b.row("Public IP", pip);
}
b = b.row("NAT Layers", &format!("~{}", nat.nat_layers_estimate));
b = b.row("CGNAT", if nat.cgnat_detected { "DETECTED" } else { "No" });
b = b.row(
"Double NAT",
if nat.double_nat_suspected {
"Suspected"
} else {
"No"
},
);
b = b.row("Assessment", &nat.assessment);
output.push_str(&b.finish());
output.push('\n');
}
if let Some(ref links) = tech.wifi {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b
.full_top_border()
.span_row(" WI-FI LINK QUALITY")
.divider();
for (i, link) in links.iter().enumerate() {
if i > 0 {
b = b.divider();
}
b = b.row("Interface", &link.interface);
if let Some(ref ssid) = link.ssid {
b = b.row("SSID", ssid);
}
if let Some(ref bssid) = link.bssid {
b = b.row("BSSID", bssid);
}
match (link.signal_pct, link.rssi_dbm) {
(Some(pct), _) => b = b.row("Signal", &format!("{}%", pct)),
(None, Some(rssi)) => b = b.row("Signal", &format!("{} dBm", rssi)),
_ => {}
}
if let (Some(ch), Some(ref band)) = (link.channel, link.band.as_ref()) {
b = b.row("Channel", &format!("{} ({})", ch, band));
} else if let Some(ref band) = link.band {
b = b.row("Band", band);
}
if let Some(ref phy) = link.phy_mode {
b = b.row("PHY Mode", phy);
}
if let Some(ref sec) = link.security {
b = b.row("Security", sec);
}
match (link.rx_rate_mbps, link.tx_rate_mbps) {
(Some(rx), Some(tx)) => {
b = b.row("Link Rate", &format!("RX {:.0} / TX {:.0} Mbps", rx, tx))
}
(Some(rx), None) => b = b.row("Link Rate", &format!("RX {:.0} Mbps", rx)),
(None, Some(tx)) => b = b.row("Link Rate", &format!("TX {:.0} Mbps", tx)),
_ => {}
}
b = b.row("Assessment", &link.assessment);
}
output.push_str(&b.finish());
output.push('\n');
}
if let Some(ref bench) = tech.dns_benchmark {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b
.full_top_border()
.span_row(" DNS RESOLVER BENCHMARK")
.divider();
for r in &bench.resolvers {
let timing = r
.avg_ms
.map(|ms| format!("{:.0}ms avg", ms))
.unwrap_or_else(|| "no replies".to_string());
b = b.row(
&r.name,
&format!("{} ({}/{} ok)", timing, r.queries_ok, r.queries_total),
);
}
b = b.divider();
if let Some(ref fastest) = bench.fastest {
b = b.row("Fastest", fastest);
}
if let Some(delta) = bench.system_vs_fastest_ms {
b = b.row("System vs Best", &format!("{:+.0}ms", delta));
}
if let Some(hijack) = bench.hijack_detected {
b = b.row("NXDOMAIN Hijack", if hijack { "DETECTED" } else { "No" });
}
if let Some(dnssec) = bench.dnssec_validating {
b = b.row(
"DNSSEC",
if dnssec {
"Validating"
} else {
"Not validating"
},
);
}
b = b.row("Assessment", &bench.assessment);
output.push_str(&b.finish());
output.push('\n');
}
if let Some(ref portal) = tech.captive_portal {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b.full_top_border().span_row(" CAPTIVE PORTAL").divider();
b = b.row(
"Portal",
if portal.portal_detected {
"DETECTED"
} else {
"Not detected"
},
);
if let Some(status) = portal.http_status {
b = b.row("Probe Status", &format!("HTTP {}", status));
}
if let Some(ref loc) = portal.redirect_location {
b = b.row("Redirects To", loc);
}
b = b.row("Assessment", &portal.assessment);
output.push_str(&b.finish());
output.push('\n');
}
if let Some(ref clock) = tech.clock_sync {
let mut b = ReportBuilder::new(label_width, data_width, chars);
b = b.full_top_border().span_row(" CLOCK SYNC (NTP)").divider();
if let Some(offset) = clock.offset_ms {
b = b.row("Clock Offset", &format!("{:+.0}ms", offset));
}
b = b.row(
"Source",
&format!(
"{} ({} server{} responded)",
clock.source,
clock.servers_responded,
if clock.servers_responded == 1 {
""
} else {
"s"
}
),
);
if clock.ntp_blocked {
b = b.row("NTP (UDP/123)", "Blocked or filtered");
}
b = b.row("Assessment", &clock.assessment);
output.push_str(&b.finish());
output.push('\n');
}
output
}
fn render_summary_row(
builder: ReportBuilder,
result: &crate::diagnostics::DiagnosticResult,
config: &Config,
) -> ReportBuilder {
let icon = config.status_chars(&result.status);
let colored_icon = colorize_status(icon, &result.status, config);
let label = format!("{} {}", colored_icon, result.category);
let lines: Vec<&str> = result.summary.split('\n').collect();
let mut b = builder.row(&label, lines[0]);
for line in lines.iter().skip(1) {
b = b.row("", line);
}
b
}
fn count_issues(results: &DiagnosticResults) -> (usize, usize) {
let statuses = [
&results.adapters.status,
&results.interfaces.status,
&results.gateway.status,
&results.dns.status,
&results.public_ip.status,
&results.latency.status,
&results.speed.status,
&results.ports.status,
];
let fails = statuses
.iter()
.filter(|s| ***s == DiagnosticStatus::Fail)
.count();
let warns = statuses
.iter()
.filter(|s| ***s == DiagnosticStatus::Warn)
.count();
(fails, warns)
}
fn format_overall(fails: usize, warns: usize, config: &Config) -> String {
if fails > 0 && warns > 0 {
let text = format!(
"{} failure{}, {} warning{}",
fails,
if fails > 1 { "s" } else { "" },
warns,
if warns > 1 { "s" } else { "" }
);
colorize_status(&text, &DiagnosticStatus::Fail, config)
} else if fails > 0 {
let text = format!(
"{} failure{} detected",
fails,
if fails > 1 { "s" } else { "" }
);
colorize_status(&text, &DiagnosticStatus::Fail, config)
} else if warns > 0 {
let text = format!(
"{} warning{} detected",
warns,
if warns > 1 { "s" } else { "" }
);
colorize_status(&text, &DiagnosticStatus::Warn, config)
} else {
colorize_status("All diagnostics passed", &DiagnosticStatus::Ok, config)
}
}
fn format_link_speed(mbps: u64) -> String {
if mbps >= 1000 {
format!("{:.1} Gbps", mbps as f64 / 1000.0)
} else {
format!("{} Mbps", mbps)
}
}
fn format_bytes(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = 1024 * KB;
const GB: u64 = 1024 * MB;
if bytes >= GB {
format!("{:.2} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.1} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.1} KB", bytes as f64 / KB as f64)
} else {
format!("{} B", bytes)
}
}