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