1use crate::config::Config;
2use crate::diagnostics::{DiagnosticResults, DiagnosticStatus, TechnicianResults};
3use crate::render::color::{colorize_status, dim};
4use crate::render::table::ReportBuilder;
5
6pub fn render(results: &DiagnosticResults, config: &Config) -> String {
7 let label_width = 16;
8 let data_width = 40;
9 let chars = config.box_chars();
10
11 let mut output = String::new();
12
13 let mut builder = ReportBuilder::new(label_width, data_width, chars)
15 .header(config.title(), config.subtitle());
16
17 builder = builder.span_row(" DIAGNOSTIC SUMMARY").divider();
18
19 builder = render_summary_row(builder, &results.adapters, config);
20 builder = render_summary_row(builder, &results.interfaces, config);
21 builder = render_summary_row(builder, &results.gateway, config);
22 builder = render_summary_row(builder, &results.dns, config);
23 builder = render_summary_row(builder, &results.public_ip, config);
24 builder = render_summary_row(builder, &results.latency, config);
25 builder = render_summary_row(builder, &results.speed, config);
26 builder = render_summary_row(builder, &results.ports, config);
27
28 let (fail_count, warn_count) = count_issues(results);
29 let overall = format_overall(fail_count, warn_count, config);
30 builder = builder.divider();
31 builder = builder.span_row(&format!(" OVERALL: {}", overall));
32
33 if fail_count > 0 {
34 builder = builder.span_row(&dim(" Run 'nd300 -f' to attempt automatic fixes", config));
35 }
36
37 output.push_str(&builder.finish());
38 output.push('\n');
39
40 if let Some(ref ifaces) = results.interface_details {
44 let mut b = ReportBuilder::new(label_width, data_width, chars);
45 b = b
46 .full_top_border()
47 .span_row(" NETWORK INTERFACES")
48 .divider();
49
50 for (i, iface) in ifaces.iter().enumerate() {
51 if i > 0 {
52 b = b.divider();
53 }
54 b = b.row("Name", &iface.name);
55 b = b.row("Type", &iface.interface_type);
56 b = b.row("MAC", &iface.mac);
57 b = b.row("Status", if iface.is_up { "Up" } else { "Down" });
58 for ip in &iface.ip_addresses {
59 b = b.row("IP Address", ip);
60 }
61 }
62 output.push_str(&b.finish());
63 output.push('\n');
64 }
65
66 if let Some(ref adapters) = results.adapter_details {
68 if !adapters.is_empty() {
69 let mut b = ReportBuilder::new(label_width, data_width, chars);
70 b = b.full_top_border().span_row(" NETWORK ADAPTERS").divider();
71
72 for (i, adapter) in adapters.iter().enumerate() {
73 if i > 0 {
74 b = b.divider();
75 }
76 let adapter_label = adapter.description.as_deref().unwrap_or(&adapter.name);
78 b = b.row("Adapter", adapter_label);
79
80 let type_detail = if let Some(ref pm) = adapter.physical_medium {
82 format!("{} ({})", adapter.adapter_type, pm)
83 } else {
84 adapter.adapter_type.clone()
85 };
86 b = b.row("Type", &type_detail);
87 b = b.row("Status", &adapter.status);
88
89 if let Some(ref mac) = adapter.mac_address {
90 b = b.row("MAC", mac);
91 }
92
93 match (adapter.link_speed_mbps, adapter.rx_link_speed_mbps) {
95 (Some(tx), Some(rx)) if tx == rx => {
96 b = b.row("Link Speed", &format_link_speed(tx));
97 }
98 (Some(tx), Some(rx)) => {
99 b = b.row(
100 "Link Speed",
101 &format!(
102 "TX: {} / RX: {}",
103 format_link_speed(tx),
104 format_link_speed(rx)
105 ),
106 );
107 }
108 (Some(tx), None) => {
109 b = b.row("Link Speed", &format_link_speed(tx));
110 }
111 _ => {}
112 }
113
114 if let Some(ref gws) = adapter.gateways {
115 for gw in gws {
116 b = b.row("Gateway", gw);
117 }
118 }
119 if let Some(ref dns) = adapter.dns_servers {
120 for server in dns {
121 b = b.row("DNS", server);
122 }
123 }
124 if let Some(mtu) = adapter.mtu {
125 b = b.row("MTU", &mtu.to_string());
126 }
127 if let Some(metric) = adapter.ipv4_metric {
128 b = b.row("IPv4 Metric", &metric.to_string());
129 }
130
131 if let Some(ref drv) = adapter.driver_name {
132 b = b.row("Driver", drv);
133 }
134 if let Some(ref ver) = adapter.driver_version {
135 b = b.row("Driver Version", ver);
136 }
137 if let Some(ref date) = adapter.driver_date {
138 b = b.row("Driver Date", date);
139 }
140 }
141 output.push_str(&b.finish());
142 output.push('\n');
143 }
144 }
145
146 if let Some(ref gw) = results.gateway_details {
148 let mut b = ReportBuilder::new(label_width, data_width, chars);
149 b = b.full_top_border().span_row(" GATEWAY").divider();
150 b = b.row("IP Address", &gw.ip);
151 b = b.row("Reachable", if gw.reachable { "Yes" } else { "No" });
152 if let Some(lat) = gw.latency_ms {
153 b = b.row("Latency", &format!("{:.1}ms", lat));
154 }
155 output.push_str(&b.finish());
156 output.push('\n');
157 }
158
159 if let Some(ref dns) = results.dns_details {
161 let mut b = ReportBuilder::new(label_width, data_width, chars);
162 b = b.full_top_border().span_row(" DNS SERVERS").divider();
163
164 for server in &dns.servers {
165 b = b.row("Server", &server.address);
166 }
167
168 if let Some(ref test) = dns.resolution_test {
169 b = b.divider();
170 b = b.row("Test Domain", &test.domain);
171 b = b.row("Resolved", if test.resolved { "Yes" } else { "No" });
172 b = b.row("Time", &format!("{:.1}ms", test.resolution_time_ms));
173 for ip in &test.resolved_ips {
174 b = b.row("Resolved IP", ip);
175 }
176 }
177 output.push_str(&b.finish());
178 output.push('\n');
179 }
180
181 if let Some(ref pip) = results.public_ip_details {
183 let mut b = ReportBuilder::new(label_width, data_width, chars);
184 b = b
185 .full_top_border()
186 .span_row(" PUBLIC IP & GEOLOCATION")
187 .divider();
188 b = b.row("Public IP", &pip.ip);
189 b = b.row("Lookup Time", &format!("{:.0}ms", pip.lookup_time_ms));
190 b = b.row("Behind NAT", if pip.behind_nat { "Yes" } else { "No" });
191
192 if let Some(ref city) = pip.city {
193 b = b.row("City", city);
194 }
195 if let Some(ref region) = pip.region {
196 b = b.row("Region", region);
197 }
198 if let Some(ref country) = pip.country {
199 b = b.row("Country", country);
200 }
201 if let Some(ref isp) = pip.isp {
202 b = b.row("ISP", isp);
203 }
204 if let Some(ref org) = pip.org {
205 b = b.row("Organization", org);
206 }
207 output.push_str(&b.finish());
208 output.push('\n');
209 }
210
211 if let Some(ref latencies) = results.latency_details {
213 let mut b = ReportBuilder::new(label_width, data_width, chars);
214 b = b.full_top_border().span_row(" LATENCY TESTS").divider();
215
216 for (i, lat) in latencies.iter().enumerate() {
217 if i > 0 {
218 b = b.divider();
219 }
220 b = b.row("Host", &format!("{} ({})", lat.host, lat.label));
221 b = b.row("Reachable", if lat.reachable { "Yes" } else { "No" });
222 if let Some(min) = lat.min_ms {
223 b = b.row("Min", &format!("{:.1}ms", min));
224 }
225 if let Some(avg) = lat.avg_ms {
226 b = b.row("Avg", &format!("{:.1}ms", avg));
227 }
228 if let Some(max) = lat.max_ms {
229 b = b.row("Max", &format!("{:.1}ms", max));
230 }
231 if let Some(jitter) = lat.jitter_ms {
232 b = b.row("Jitter", &format!("{:.1}ms", jitter));
233 }
234 b = b.row("Packet Loss", &format!("{:.0}%", lat.packet_loss));
235 }
236 output.push_str(&b.finish());
237 output.push('\n');
238 }
239
240 if let Some(ref speed) = results.speed_details {
242 let mut b = ReportBuilder::new(label_width, data_width, chars);
243 b = b.full_top_border().span_row(" SPEED TEST").divider();
244
245 if let Some(ping) = speed.ping_ms {
246 b = b.row("Ping", &format!("{:.1} ms", ping));
247 }
248 if let Some(jitter) = speed.jitter_ms {
249 b = b.row("Jitter", &format!("{:.1} ms", jitter));
250 }
251 b = b.row(
252 "Download",
253 &format!(
254 "{} (avg)",
255 crate::speedtest::format_mbps(speed.download_mbps)
256 ),
257 );
258 b = b.row(
259 "Upload",
260 &format!("{} (avg)", crate::speedtest::format_mbps(speed.upload_mbps)),
261 );
262 if let Some(loss) = speed.packet_loss_pct {
263 b = b.row("Packet Loss", &format!("{:.0}%", loss));
264 }
265 b = b.row("Duration", &format!("{:.1}s", speed.duration_s));
266
267 for provider in &speed.providers {
269 if provider.error.is_some() {
270 continue;
271 }
272 b = b.section_header(&provider.provider);
273 b = b.row("Server", &provider.server);
274 if let Some(ref location) = provider.location {
275 b = b.row("Location", location);
276 }
277 if let Some(dl) = provider.download_mbps {
278 b = b.row("Download", &crate::speedtest::format_mbps(dl));
279 }
280 if let Some(ul) = provider.upload_mbps {
281 b = b.row("Upload", &crate::speedtest::format_mbps(ul));
282 }
283 b = b.row("DL Data", &format_bytes(provider.download_bytes));
284 b = b.row("UL Data", &format_bytes(provider.upload_bytes));
285 }
286
287 output.push_str(&b.finish());
288 output.push('\n');
289 }
290
291 if let Some(ref ports) = results.port_details {
293 let mut b = ReportBuilder::new(label_width, data_width, chars);
294 b = b
295 .full_top_border()
296 .span_row(" PORT CONNECTIVITY")
297 .divider();
298
299 for port in ports {
300 let status = if port.open { "Open" } else { "Blocked" };
301 let lat = port
302 .latency_ms
303 .map(|l| format!(" ({:.0}ms)", l))
304 .unwrap_or_default();
305 b = b.row(
306 &format!("{} ({})", port.service, port.port),
307 &format!("{}{}", status, lat),
308 );
309 }
310 output.push_str(&b.finish());
311 output.push('\n');
312 }
313
314 if let Some(ref tech) = results.technician {
316 output.push_str(&render_technician_details(tech, config));
317 }
318
319 output.push_str(&format!(" Report generated: {}\n", results.timestamp));
321 output.push('\n');
322
323 output
324}
325
326fn render_technician_details(tech: &TechnicianResults, config: &Config) -> String {
327 let label_width = 16;
328 let data_width = 40;
329 let chars = config.box_chars();
330 let mut output = String::new();
331
332 if let Some(ref arp) = tech.arp_table {
334 if !arp.is_empty() {
335 let mut b = ReportBuilder::new(label_width, data_width, chars);
336 b = b.full_top_border().span_row(" ARP TABLE").divider();
337 for entry in arp.iter().take(30) {
338 b = b.row(&entry.ip, &format!("{} ({})", entry.mac, entry.entry_type));
339 }
340 if arp.len() > 30 {
341 b = b.row("", &format!("... and {} more", arp.len() - 30));
342 }
343 output.push_str(&b.finish());
344 output.push('\n');
345 }
346 }
347
348 if let Some(ref routes) = tech.routing_table {
350 if !routes.is_empty() {
351 let mut b = ReportBuilder::new(label_width, data_width, chars);
352 b = b.full_top_border().span_row(" ROUTING TABLE").divider();
353 for route in routes.iter().take(20) {
354 let gw = if route.gateway.is_empty() {
355 "direct".to_string()
356 } else {
357 route.gateway.clone()
358 };
359 let metric = route
360 .metric
361 .map(|m| format!(" m:{}", m))
362 .unwrap_or_default();
363 b = b.row(
364 &route.destination,
365 &format!("via {} dev {}{}", gw, route.interface, metric),
366 );
367 }
368 if routes.len() > 20 {
369 b = b.row("", &format!("... and {} more", routes.len() - 20));
370 }
371 output.push_str(&b.finish());
372 output.push('\n');
373 }
374 }
375
376 if let Some(ref conns) = tech.active_connections {
378 if !conns.is_empty() {
379 let established: Vec<_> = conns
380 .iter()
381 .filter(|c| c.state == "ESTABLISHED")
382 .take(20)
383 .collect();
384 if !established.is_empty() {
385 let mut b = ReportBuilder::new(label_width, data_width, chars);
386 b = b
387 .full_top_border()
388 .span_row(" ACTIVE CONNECTIONS (ESTABLISHED)")
389 .divider();
390 for conn in &established {
391 let proc_info = conn.process_name.as_deref().unwrap_or("?");
392 b = b.row(
393 &conn.local_addr,
394 &format!("{} [{}]", conn.remote_addr, proc_info),
395 );
396 }
397 let total_established = conns.iter().filter(|c| c.state == "ESTABLISHED").count();
398 if total_established > 20 {
399 b = b.row("", &format!("... and {} more", total_established - 20));
400 }
401 output.push_str(&b.finish());
402 output.push('\n');
403 }
404 }
405 }
406
407 if let Some(ref ports) = tech.listening_ports {
409 if !ports.is_empty() {
410 let mut b = ReportBuilder::new(label_width, data_width, chars);
411 b = b.full_top_border().span_row(" LISTENING PORTS").divider();
412 for port in ports.iter().take(20) {
413 let proc_info = port.process_name.as_deref().unwrap_or("?");
414 b = b.row(
415 &format!("{} :{}", port.protocol, port.port),
416 &format!("{} [{}]", port.address, proc_info),
417 );
418 }
419 if ports.len() > 20 {
420 b = b.row("", &format!("... and {} more", ports.len() - 20));
421 }
422 output.push_str(&b.finish());
423 output.push('\n');
424 }
425 }
426
427 if let Some(ref dhcp) = tech.dhcp_info {
429 if !dhcp.is_empty() {
430 let mut b = ReportBuilder::new(label_width, data_width, chars);
431 b = b.full_top_border().span_row(" DHCP LEASES").divider();
432 for (i, lease) in dhcp.iter().enumerate() {
433 if i > 0 {
434 b = b.divider();
435 }
436 b = b.row("Interface", &lease.interface);
437 b = b.row(
438 "DHCP Enabled",
439 if lease.dhcp_enabled { "Yes" } else { "No" },
440 );
441 if let Some(ref server) = lease.dhcp_server {
442 b = b.row("DHCP Server", server);
443 }
444 if let Some(ref ip) = lease.ip_address {
445 b = b.row("IP Address", ip);
446 }
447 if let Some(ref obtained) = lease.lease_obtained {
448 b = b.row("Obtained", obtained);
449 }
450 if let Some(ref expires) = lease.lease_expires {
451 b = b.row("Expires", expires);
452 }
453 }
454 output.push_str(&b.finish());
455 output.push('\n');
456 }
457 }
458
459 if let Some(ref stats) = tech.protocol_stats {
461 let mut b = ReportBuilder::new(label_width, data_width, chars);
462 b = b
463 .full_top_border()
464 .span_row(" PROTOCOL STATISTICS")
465 .divider();
466
467 b = b.row("TCP Active Opens", &stats.tcp.active_opens.to_string());
468 b = b.row("TCP Passive Opens", &stats.tcp.passive_opens.to_string());
469 b = b.row("TCP Current", &stats.tcp.current_connections.to_string());
470 b = b.row("TCP Failed", &stats.tcp.failed_connections.to_string());
471 b = b.row("TCP Resets", &stats.tcp.reset_connections.to_string());
472 b = b.row(
473 "TCP Retransmits",
474 &stats.tcp.segments_retransmitted.to_string(),
475 );
476 b = b.row("TCP Segments In", &stats.tcp.segments_received.to_string());
477 b = b.row("TCP Segments Out", &stats.tcp.segments_sent.to_string());
478 b = b.divider();
479 b = b.row("UDP In", &stats.udp.datagrams_received.to_string());
480 b = b.row("UDP Out", &stats.udp.datagrams_sent.to_string());
481 b = b.row("UDP Errors", &stats.udp.receive_errors.to_string());
482 b = b.divider();
483 b = b.row("ICMP In", &stats.icmp.messages_received.to_string());
484 b = b.row("ICMP Out", &stats.icmp.messages_sent.to_string());
485 b = b.row("ICMP Errors In", &stats.icmp.errors_received.to_string());
486
487 output.push_str(&b.finish());
488 output.push('\n');
489 }
490
491 if let Some(ref hw) = tech.adapter_hw_stats {
493 if !hw.is_empty() {
494 let mut b = ReportBuilder::new(label_width, data_width, chars);
495 b = b
496 .full_top_border()
497 .span_row(" ADAPTER HARDWARE STATS")
498 .divider();
499 for (i, stat) in hw.iter().enumerate() {
500 if i > 0 {
501 b = b.divider();
502 }
503 b = b.row("Interface", &stat.name);
504 b = b.row("RX Bytes", &format_bytes(stat.rx_bytes));
505 b = b.row("TX Bytes", &format_bytes(stat.tx_bytes));
506 b = b.row("RX Packets", &stat.rx_packets.to_string());
507 b = b.row("TX Packets", &stat.tx_packets.to_string());
508 b = b.row("RX Errors", &stat.rx_errors.to_string());
509 b = b.row("TX Errors", &stat.tx_errors.to_string());
510 if let Some(ref speed) = stat.link_speed {
511 b = b.row("Link Speed", speed);
512 }
513 if let Some(ref dup) = stat.duplex {
514 b = b.row("Duplex", dup);
515 }
516 }
517 output.push_str(&b.finish());
518 output.push('\n');
519 }
520 }
521
522 if let Some(ref proxy) = tech.proxy_config {
524 let mut b = ReportBuilder::new(label_width, data_width, chars);
525 b = b
526 .full_top_border()
527 .span_row(" PROXY CONFIGURATION")
528 .divider();
529 b = b.row(
530 "Proxy Enabled",
531 if proxy.proxy_enabled { "Yes" } else { "No" },
532 );
533 if let Some(ref http) = proxy.http_proxy {
534 b = b.row("HTTP Proxy", http);
535 }
536 if let Some(ref https) = proxy.https_proxy {
537 b = b.row("HTTPS Proxy", https);
538 }
539 if let Some(ref socks) = proxy.socks_proxy {
540 b = b.row("SOCKS Proxy", socks);
541 }
542 if let Some(ref pac) = proxy.pac_url {
543 b = b.row("PAC URL", pac);
544 }
545 if let Some(ref no) = proxy.no_proxy {
546 b = b.row("No Proxy", no);
547 }
548 output.push_str(&b.finish());
549 output.push('\n');
550 }
551
552 if let Some(ref vpns) = tech.vpn_info {
554 let mut b = ReportBuilder::new(label_width, data_width, chars);
555 b = b.full_top_border().span_row(" VPN DETECTION").divider();
556 for (i, vpn) in vpns.iter().enumerate() {
557 if i > 0 {
558 b = b.divider();
559 }
560 b = b.row("VPN Adapter", &vpn.name);
561 b = b.row("Type", &vpn.adapter_type);
562 b = b.row("Status", &vpn.status);
563 if let Some(ref vendor) = vpn.vendor {
564 b = b.row("Vendor", vendor);
565 }
566 if vpn.is_enterprise {
567 b = b.row("Policy", "Enterprise/Managed");
568 }
569 if let Some(ref iface) = vpn.interface_name {
570 b = b.row("Interface", iface);
571 }
572 if let Some(ref ip) = vpn.ip_address {
573 b = b.row("IP Address", ip);
574 }
575 }
576 output.push_str(&b.finish());
577 output.push('\n');
578 }
579
580 if let Some(ref fw) = tech.firewall_info {
582 let mut b = ReportBuilder::new(label_width, data_width, chars);
583 b = b.full_top_border().span_row(" FIREWALL STATUS").divider();
584 b = b.row("Status", &fw.summary);
585 for profile in &fw.profiles {
586 b = b.row(
587 &profile.name,
588 if profile.enabled {
589 "Enabled"
590 } else {
591 "Disabled"
592 },
593 );
594 }
595 output.push_str(&b.finish());
596 output.push('\n');
597 }
598
599 if let Some(ref cache) = tech.dns_cache {
601 if !cache.is_empty() {
602 let mut b = ReportBuilder::new(label_width, data_width, chars);
603 b = b.full_top_border().span_row(" DNS CACHE").divider();
604 for entry in cache.iter().take(20) {
605 b = b.row(
606 &entry.name,
607 &format!("{}: {}", entry.record_type, entry.data),
608 );
609 }
610 if cache.len() > 20 {
611 b = b.row("", &format!("... and {} more entries", cache.len() - 20));
612 }
613 output.push_str(&b.finish());
614 output.push('\n');
615 }
616 }
617
618 if let Some(ref ipv6) = tech.ipv6_info {
620 let mut b = ReportBuilder::new(label_width, data_width, chars);
621 b = b.full_top_border().span_row(" IPv6 STATUS").divider();
622 b = b.row("Available", if ipv6.available { "Yes" } else { "No" });
623 let conn_str = match ipv6.connectivity {
624 crate::diagnostics::ipv6::Ipv6Connectivity::Full => "Full",
625 crate::diagnostics::ipv6::Ipv6Connectivity::LinkLocal => "Link-local only",
626 crate::diagnostics::ipv6::Ipv6Connectivity::None => "None",
627 };
628 b = b.row("Connectivity", conn_str);
629 b = b.row("Dual Stack", if ipv6.dual_stack { "Yes" } else { "No" });
630
631 for addr in ipv6.addresses.iter().take(10) {
632 b = b.row(
633 &addr.scope,
634 &format!("{} ({})", addr.address, addr.interface),
635 );
636 }
637 output.push_str(&b.finish());
638 output.push('\n');
639 }
640
641 if let Some(ref mtus) = tech.mtu_info {
643 if !mtus.is_empty() {
644 let mut b = ReportBuilder::new(label_width, data_width, chars);
645 b = b
646 .full_top_border()
647 .span_row(" MTU PER INTERFACE")
648 .divider();
649 for mtu in mtus {
650 b = b.row(&mtu.interface, &mtu.mtu.to_string());
651 }
652 output.push_str(&b.finish());
653 output.push('\n');
654 }
655 }
656
657 if let Some(ref states) = tech.connection_states {
659 let mut b = ReportBuilder::new(label_width, data_width, chars);
660 b = b
661 .full_top_border()
662 .span_row(" TCP CONNECTION STATES")
663 .divider();
664 b = b.row("ESTABLISHED", &states.established.to_string());
665 b = b.row("TIME_WAIT", &states.time_wait.to_string());
666 b = b.row("CLOSE_WAIT", &states.close_wait.to_string());
667 b = b.row("FIN_WAIT_1", &states.fin_wait_1.to_string());
668 b = b.row("FIN_WAIT_2", &states.fin_wait_2.to_string());
669 b = b.row("SYN_SENT", &states.syn_sent.to_string());
670 b = b.row("SYN_RECEIVED", &states.syn_received.to_string());
671 b = b.row("CLOSING", &states.closing.to_string());
672 b = b.row("LAST_ACK", &states.last_ack.to_string());
673 b = b.row("LISTEN", &states.listen.to_string());
674 output.push_str(&b.finish());
675 output.push('\n');
676 }
677
678 if let Some(ref bb) = tech.bufferbloat {
680 let mut b = ReportBuilder::new(label_width, data_width, chars);
681 b = b.full_top_border().span_row(" BUFFERBLOAT TEST").divider();
682 b = b.row("Grade", &bb.grade);
683 b = b.row(
684 "Unloaded Latency",
685 &format!("{:.1}ms", bb.unloaded_latency_ms),
686 );
687 if let Some(loaded) = bb.loaded_latency_ms {
688 b = b.row("Loaded Latency", &format!("{:.1}ms", loaded));
689 }
690 if let Some(bloat) = bb.bloat_ms {
691 b = b.row("Bloat", &format!("+{:.1}ms", bloat));
692 }
693 b = b.row("Assessment", &bb.description);
694 output.push_str(&b.finish());
695 output.push('\n');
696 }
697
698 if let Some(ref rdns) = tech.reverse_dns {
700 if !rdns.is_empty() {
701 let mut b = ReportBuilder::new(label_width, data_width, chars);
702 b = b.full_top_border().span_row(" REVERSE DNS").divider();
703 for entry in rdns {
704 let hostname = entry.hostname.as_deref().unwrap_or("(no PTR)");
705 b = b.row(&format!("{} ({})", entry.label, entry.ip), hostname);
706 }
707 output.push_str(&b.finish());
708 output.push('\n');
709 }
710 }
711
712 if let Some(ref tls) = tech.tls_inspection {
714 let mut b = ReportBuilder::new(label_width, data_width, chars);
715 b = b
716 .full_top_border()
717 .span_row(" TLS INSPECTION CHECK")
718 .divider();
719 b = b.row("Intercepted", if tls.detected { "DETECTED" } else { "No" });
720 b = b.row("Assessment", &tls.description);
721 output.push_str(&b.finish());
722 output.push('\n');
723 }
724
725 if let Some(ref traffic) = tech.traffic_counters {
727 if !traffic.is_empty() {
728 let mut b = ReportBuilder::new(label_width, data_width, chars);
729 b = b
730 .full_top_border()
731 .span_row(" TRAFFIC COUNTERS (since boot)")
732 .divider();
733 for counter in traffic {
734 b = b.row(
735 &counter.interface,
736 &format!("RX {} / TX {}", counter.rx_formatted, counter.tx_formatted),
737 );
738 }
739 output.push_str(&b.finish());
740 output.push('\n');
741 }
742 }
743
744 output
745}
746
747fn render_summary_row(
748 builder: ReportBuilder,
749 result: &crate::diagnostics::DiagnosticResult,
750 config: &Config,
751) -> ReportBuilder {
752 let icon = config.status_chars(&result.status);
753 let colored_icon = colorize_status(icon, &result.status, config);
754 let label = format!("{} {}", colored_icon, result.category);
755
756 let lines: Vec<&str> = result.summary.split('\n').collect();
757 let mut b = builder.row(&label, lines[0]);
758 for line in lines.iter().skip(1) {
759 b = b.row("", line);
760 }
761 b
762}
763
764fn count_issues(results: &DiagnosticResults) -> (usize, usize) {
765 let statuses = [
766 &results.adapters.status,
767 &results.interfaces.status,
768 &results.gateway.status,
769 &results.dns.status,
770 &results.public_ip.status,
771 &results.latency.status,
772 &results.speed.status,
773 &results.ports.status,
774 ];
775
776 let fails = statuses
777 .iter()
778 .filter(|s| ***s == DiagnosticStatus::Fail)
779 .count();
780 let warns = statuses
781 .iter()
782 .filter(|s| ***s == DiagnosticStatus::Warn)
783 .count();
784 (fails, warns)
785}
786
787fn format_overall(fails: usize, warns: usize, config: &Config) -> String {
788 if fails > 0 && warns > 0 {
789 let text = format!(
790 "{} failure{}, {} warning{}",
791 fails,
792 if fails > 1 { "s" } else { "" },
793 warns,
794 if warns > 1 { "s" } else { "" }
795 );
796 colorize_status(&text, &DiagnosticStatus::Fail, config)
797 } else if fails > 0 {
798 let text = format!(
799 "{} failure{} detected",
800 fails,
801 if fails > 1 { "s" } else { "" }
802 );
803 colorize_status(&text, &DiagnosticStatus::Fail, config)
804 } else if warns > 0 {
805 let text = format!(
806 "{} warning{} detected",
807 warns,
808 if warns > 1 { "s" } else { "" }
809 );
810 colorize_status(&text, &DiagnosticStatus::Warn, config)
811 } else {
812 colorize_status("All diagnostics passed", &DiagnosticStatus::Ok, config)
813 }
814}
815
816fn format_link_speed(mbps: u64) -> String {
817 if mbps >= 1000 {
818 format!("{:.1} Gbps", mbps as f64 / 1000.0)
819 } else {
820 format!("{} Mbps", mbps)
821 }
822}
823
824fn format_bytes(bytes: u64) -> String {
825 const KB: u64 = 1024;
826 const MB: u64 = 1024 * KB;
827 const GB: u64 = 1024 * MB;
828
829 if bytes >= GB {
830 format!("{:.2} GB", bytes as f64 / GB as f64)
831 } else if bytes >= MB {
832 format!("{:.1} MB", bytes as f64 / MB as f64)
833 } else if bytes >= KB {
834 format!("{:.1} KB", bytes as f64 / KB as f64)
835 } else {
836 format!("{} B", bytes)
837 }
838}