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