1use crate::{
14 nat_traversal_api::{NatTraversalEvent, NatTraversalStatistics, PeerId},
15 terminal_ui,
16};
17
18#[derive(Debug, Clone, Default)]
20pub struct NodeStats {
21 pub active_connections: usize,
23 pub successful_connections: usize,
25 pub failed_connections: usize,
27}
28use std::{
29 collections::{HashMap, VecDeque},
30 net::SocketAddr,
31 sync::Arc,
32 time::{Duration, Instant},
33};
34use tokio::sync::RwLock;
35
36#[derive(Debug, Clone, Copy)]
38pub enum BoxStyle {
39 Single,
41 Double,
43 Rounded,
45}
46
47fn draw_box(title: &str, content: &str, _style: BoxStyle, width: usize) -> String {
49 let mut result = String::new();
50
51 let padding = width.saturating_sub(title.len() + 4);
53 let left_pad = padding / 2;
54 let right_pad = padding - left_pad;
55
56 result.push_str(&format!(
57 "╭{} {} {}╮\n",
58 "─".repeat(left_pad),
59 title,
60 "─".repeat(right_pad)
61 ));
62
63 for line in content.lines() {
65 let line_len = line.chars().count();
66 let padding = width.saturating_sub(line_len + 2);
67 result.push_str(&format!("│ {}{} │\n", line, " ".repeat(padding)));
68 }
69
70 result.push_str(&format!("╰{}╯", "─".repeat(width - 2)));
72
73 result
74}
75
76#[derive(Debug, Clone)]
78pub struct DashboardConfig {
79 pub update_interval: Duration,
81 pub history_size: usize,
83 pub detailed_tracking: bool,
85 pub show_graphs: bool,
87}
88
89impl Default for DashboardConfig {
90 fn default() -> Self {
91 Self {
92 update_interval: Duration::from_secs(1),
93 history_size: 60, detailed_tracking: true,
95 show_graphs: true,
96 }
97 }
98}
99
100#[derive(Debug, Clone)]
102pub struct ConnectionInfo {
103 pub peer_id: PeerId,
105 pub remote_address: SocketAddr,
107 pub connected_at: Instant,
109 pub bytes_sent: u64,
111 pub bytes_received: u64,
113 pub last_activity: Instant,
115 pub rtt: Option<Duration>,
117 pub packet_loss: f64,
119 pub nat_type: String,
121}
122
123#[derive(Debug, Clone)]
125struct DataPoint {
126 #[allow(dead_code)]
127 timestamp: Instant,
128 active_connections: usize,
129 nat_success_rate: f64,
130 #[allow(dead_code)]
131 bytes_per_second: u64,
132 #[allow(dead_code)]
133 avg_rtt: Duration,
134}
135
136pub struct StatsDashboard {
138 config: DashboardConfig,
139 node_stats: Arc<RwLock<NodeStats>>,
141 nat_stats: Arc<RwLock<NatTraversalStatistics>>,
143 connections: Arc<RwLock<HashMap<PeerId, ConnectionInfo>>>,
145 #[allow(dead_code)]
147 history: Arc<RwLock<VecDeque<DataPoint>>>,
148 start_time: Instant,
150 last_update: Arc<RwLock<Instant>>,
152}
153
154impl StatsDashboard {
155 pub fn new(config: DashboardConfig) -> Self {
157 let history_size = config.history_size;
158 Self {
159 config,
160 node_stats: Arc::new(RwLock::new(NodeStats::default())),
161 nat_stats: Arc::new(RwLock::new(NatTraversalStatistics::default())),
162 connections: Arc::new(RwLock::new(HashMap::new())),
163 history: Arc::new(RwLock::new(VecDeque::with_capacity(history_size))),
164 start_time: Instant::now(),
165 last_update: Arc::new(RwLock::new(Instant::now())),
166 }
167 }
168
169 pub fn config(&self) -> &DashboardConfig {
171 &self.config
172 }
173
174 pub async fn update_node_stats(&self, stats: NodeStats) {
176 *self.node_stats.write().await = stats;
177 }
178
179 pub async fn update_nat_stats(&self, stats: NatTraversalStatistics) {
181 *self.nat_stats.write().await = stats;
182 }
183
184 pub async fn handle_nat_event(&self, event: &NatTraversalEvent) {
186 match event {
187 NatTraversalEvent::ConnectionEstablished {
188 peer_id,
189 remote_address,
190 } => {
191 let mut connections = self.connections.write().await;
192 connections.insert(
193 *peer_id,
194 ConnectionInfo {
195 peer_id: *peer_id,
196 remote_address: *remote_address,
197 connected_at: Instant::now(),
198 bytes_sent: 0,
199 bytes_received: 0,
200 last_activity: Instant::now(),
201 rtt: None,
202 packet_loss: 0.0,
203 nat_type: "Unknown".to_string(),
204 },
205 );
206 }
207 NatTraversalEvent::TraversalFailed { peer_id, .. } => {
208 let mut connections = self.connections.write().await;
209 connections.remove(peer_id);
210 }
211 _ => {}
212 }
213 }
214
215 pub async fn update_connection_metrics(
217 &self,
218 peer_id: PeerId,
219 bytes_sent: u64,
220 bytes_received: u64,
221 rtt: Option<Duration>,
222 ) {
223 let mut connections = self.connections.write().await;
224 if let Some(conn) = connections.get_mut(&peer_id) {
225 conn.bytes_sent = bytes_sent;
226 conn.bytes_received = bytes_received;
227 conn.rtt = rtt;
228 conn.last_activity = Instant::now();
229 }
230 }
231
232 async fn record_data_point(&self) {
234 let _node_stats = self.node_stats.read().await;
235 let nat_stats = self.nat_stats.read().await;
236 let connections = self.connections.read().await;
237
238 let success_rate = if nat_stats.total_attempts > 0 {
239 nat_stats.successful_connections as f64 / nat_stats.total_attempts as f64
240 } else {
241 0.0
242 };
243
244 let total_bytes: u64 = connections
245 .values()
246 .map(|c| c.bytes_sent + c.bytes_received)
247 .sum();
248
249 let avg_rtt = if connections.is_empty() {
250 Duration::from_millis(0)
251 } else {
252 let total_rtt: Duration = connections.values().filter_map(|c| c.rtt).sum();
253 let count = connections.values().filter(|c| c.rtt.is_some()).count();
254 if count > 0 {
255 total_rtt / count as u32
256 } else {
257 Duration::from_millis(0)
258 }
259 };
260
261 let data_point = DataPoint {
262 timestamp: Instant::now(),
263 active_connections: connections.len(),
264 nat_success_rate: success_rate,
265 bytes_per_second: total_bytes,
266 avg_rtt,
267 };
268
269 let mut history = self.history.write().await;
270 if history.len() >= self.config.history_size {
271 history.pop_front();
272 }
273 history.push_back(data_point);
274 }
275
276 pub async fn render(&self) -> String {
278 self.record_data_point().await;
280
281 let mut output = String::new();
282
283 output.push_str("\x1B[2J\x1B[H");
285
286 output.push_str(&format!(
288 "{}🚀 ant-quic Connection Statistics Dashboard\n\n{}",
289 terminal_ui::colors::BOLD,
290 terminal_ui::colors::RESET
291 ));
292
293 let uptime = self.start_time.elapsed();
295 output.push_str(&format!("⏱️ Uptime: {}\n\n", format_duration(uptime)));
296
297 output.push_str(&self.render_overview_section().await);
299 output.push_str(&self.render_nat_section().await);
300 output.push_str(&self.render_connections_section().await);
301
302 if self.config.show_graphs {
303 output.push_str(&self.render_graphs_section().await);
304 }
305
306 output.push_str(&self.render_footer().await);
307
308 output
309 }
310
311 async fn render_overview_section(&self) -> String {
313 let node_stats = self.node_stats.read().await;
314 let _connections = self.connections.read().await;
315
316 let mut section = String::new();
317
318 section.push_str(&draw_box(
319 "📊 Overview",
320 &format!(
321 "Active Connections: {}\n\
322 Total Successful: {}\n\
323 Total Failed: {}\n\
324 Success Rate: {:.1}%",
325 format!(
326 "{}{}{}",
327 terminal_ui::colors::GREEN,
328 node_stats.active_connections,
329 terminal_ui::colors::RESET
330 ),
331 node_stats.successful_connections,
332 node_stats.failed_connections,
333 if node_stats.successful_connections + node_stats.failed_connections > 0 {
334 (node_stats.successful_connections as f64
335 / (node_stats.successful_connections + node_stats.failed_connections)
336 as f64)
337 * 100.0
338 } else {
339 0.0
340 }
341 ),
342 BoxStyle::Single,
343 50,
344 ));
345
346 section.push('\n');
347 section
348 }
349
350 async fn render_nat_section(&self) -> String {
352 let nat_stats = self.nat_stats.read().await;
353
354 let mut section = String::new();
355
356 section.push_str(&draw_box(
357 "🌐 NAT Traversal",
358 &format!(
359 "Total Attempts: {}\n\
360 Successful: {} ({:.1}%)\n\
361 Direct Connections: {}\n\
362 Relayed: {}\n\
363 Average Time: {:?}\n\
364 Active Sessions: {}",
365 nat_stats.total_attempts,
366 nat_stats.successful_connections,
367 if nat_stats.total_attempts > 0 {
368 (nat_stats.successful_connections as f64 / nat_stats.total_attempts as f64)
369 * 100.0
370 } else {
371 0.0
372 },
373 nat_stats.direct_connections,
374 nat_stats.relayed_connections,
375 nat_stats.average_coordination_time,
376 nat_stats.active_sessions,
377 ),
378 BoxStyle::Single,
379 50,
380 ));
381
382 section.push('\n');
383 section
384 }
385
386 async fn render_connections_section(&self) -> String {
388 let connections = self.connections.read().await;
389
390 let mut section = String::new();
391
392 if connections.is_empty() {
393 section.push_str(&draw_box(
394 "🔗 Active Connections",
395 "No active connections",
396 BoxStyle::Single,
397 50,
398 ));
399 } else {
400 let mut content = String::new();
401 for (i, (peer_id, conn)) in connections.iter().enumerate() {
402 if i > 0 {
403 content.push_str("\n─────────────────────────────────────────────\n");
404 }
405
406 content.push_str(&format!(
407 "Peer: {}\n\
408 Address: {}\n\
409 Duration: {}\n\
410 Sent: {} | Received: {}\n\
411 RTT: {} | Loss: {:.1}%",
412 format!(
413 "{}{}{}",
414 terminal_ui::colors::DIM,
415 hex::encode(&peer_id.0[..8]),
416 terminal_ui::colors::RESET
417 ),
418 conn.remote_address,
419 format_duration(conn.connected_at.elapsed()),
420 format_bytes(conn.bytes_sent),
421 format_bytes(conn.bytes_received),
422 conn.rtt
423 .map(|d| format!("{d:?}"))
424 .unwrap_or_else(|| "N/A".to_string()),
425 conn.packet_loss * 100.0,
426 ));
427 }
428
429 section.push_str(&draw_box(
430 &format!("🔗 Active Connections ({})", connections.len()),
431 &content,
432 BoxStyle::Single,
433 50,
434 ));
435 }
436
437 section.push('\n');
438 section
439 }
440
441 async fn render_graphs_section(&self) -> String {
443 let history = self.history.read().await;
444
445 if history.len() < 2 {
446 return String::new();
447 }
448
449 let mut section = String::new();
450
451 let conn_data: Vec<usize> = history.iter().map(|d| d.active_connections).collect();
453
454 section.push_str(&draw_box(
455 "📈 Connection History",
456 &render_mini_graph(&conn_data, 20, 50),
457 BoxStyle::Single,
458 50,
459 ));
460 section.push('\n');
461
462 let success_data: Vec<f64> = history.iter().map(|d| d.nat_success_rate * 100.0).collect();
464
465 section.push_str(&draw_box(
466 "📈 NAT Success Rate %",
467 &render_mini_graph_float(&success_data, 20, 50),
468 BoxStyle::Single,
469 50,
470 ));
471 section.push('\n');
472
473 section
474 }
475
476 async fn render_footer(&self) -> String {
478 let last_update = *self.last_update.read().await;
479
480 format!(
481 "\n{}\n{}",
482 format!(
483 "{}Last updated: {:?} ago{}",
484 terminal_ui::colors::DIM,
485 last_update.elapsed(),
486 terminal_ui::colors::RESET
487 ),
488 format!(
489 "{}Press Ctrl+C to exit{}",
490 terminal_ui::colors::DIM,
491 terminal_ui::colors::RESET
492 ),
493 )
494 }
495}
496
497fn format_duration(duration: Duration) -> String {
499 let secs = duration.as_secs();
500 if secs < 60 {
501 format!("{secs}s")
502 } else if secs < 3600 {
503 format!("{}m {}s", secs / 60, secs % 60)
504 } else {
505 format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
506 }
507}
508
509fn format_bytes(bytes: u64) -> String {
511 const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
512 let mut size = bytes as f64;
513 let mut unit_index = 0;
514
515 while size >= 1024.0 && unit_index < UNITS.len() - 1 {
516 size /= 1024.0;
517 unit_index += 1;
518 }
519
520 format!("{:.2} {}", size, UNITS[unit_index])
521}
522
523fn render_mini_graph(data: &[usize], height: usize, width: usize) -> String {
525 if data.is_empty() {
526 return "No data".to_string();
527 }
528
529 let max_val = *data.iter().max().unwrap_or(&1).max(&1) as f64;
530 let step = data.len().max(1) / width.min(data.len()).max(1);
531
532 let mut graph = vec![vec![' '; width]; height];
533
534 for (i, chunk) in data.chunks(step).enumerate() {
535 if i >= width {
536 break;
537 }
538
539 let avg = chunk.iter().sum::<usize>() as f64 / chunk.len() as f64;
540 let normalized = (avg / max_val * (height - 1) as f64).round() as usize;
541
542 for y in 0..=normalized {
543 let row = height - 1 - y;
544 graph[row][i] = '█';
545 }
546 }
547
548 let mut output = String::new();
549 for row in graph {
550 output.push_str(&row.iter().collect::<String>());
551 output.push('\n');
552 }
553
554 output.push_str(&format!(
555 "Max: {} | Latest: {}",
556 data.iter().max().unwrap_or(&0),
557 data.last().unwrap_or(&0)
558 ));
559
560 output
561}
562
563fn render_mini_graph_float(data: &[f64], height: usize, width: usize) -> String {
565 if data.is_empty() {
566 return "No data".to_string();
567 }
568
569 let max_val = data
570 .iter()
571 .cloned()
572 .fold(f64::NEG_INFINITY, f64::max)
573 .max(1.0);
574 let step = data.len().max(1) / width.min(data.len()).max(1);
575
576 let mut graph = vec![vec![' '; width]; height];
577
578 for (i, chunk) in data.chunks(step).enumerate() {
579 if i >= width {
580 break;
581 }
582
583 let avg = chunk.iter().sum::<f64>() / chunk.len() as f64;
584 let normalized = (avg / max_val * (height - 1) as f64).round() as usize;
585
586 for y in 0..=normalized {
587 let row = height - 1 - y;
588 graph[row][i] = '█';
589 }
590 }
591
592 let mut output = String::new();
593 for row in graph {
594 output.push_str(&row.iter().collect::<String>());
595 output.push('\n');
596 }
597
598 output.push_str(&format!(
599 "Max: {:.1}% | Latest: {:.1}%",
600 data.iter().cloned().fold(f64::NEG_INFINITY, f64::max),
601 data.last().unwrap_or(&0.0)
602 ));
603
604 output
605}