1use crate::{
7 nat_traversal_api::{PeerId, NatTraversalStatistics, NatTraversalEvent},
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 { peer_id, remote_address } => {
155 let mut connections = self.connections.write().await;
156 connections.insert(*peer_id, ConnectionInfo {
157 peer_id: *peer_id,
158 remote_address: *remote_address,
159 connected_at: Instant::now(),
160 bytes_sent: 0,
161 bytes_received: 0,
162 last_activity: Instant::now(),
163 rtt: None,
164 packet_loss: 0.0,
165 nat_type: "Unknown".to_string(),
166 });
167 }
168 NatTraversalEvent::TraversalFailed { peer_id, .. } => {
169 let mut connections = self.connections.write().await;
170 connections.remove(peer_id);
171 }
172 _ => {}
173 }
174 }
175
176 pub async fn update_connection_metrics(
178 &self,
179 peer_id: PeerId,
180 bytes_sent: u64,
181 bytes_received: u64,
182 rtt: Option<Duration>,
183 ) {
184 let mut connections = self.connections.write().await;
185 if let Some(conn) = connections.get_mut(&peer_id) {
186 conn.bytes_sent = bytes_sent;
187 conn.bytes_received = bytes_received;
188 conn.rtt = rtt;
189 conn.last_activity = Instant::now();
190 }
191 }
192
193 async fn record_data_point(&self) {
195 let _node_stats = self.node_stats.read().await;
196 let nat_stats = self.nat_stats.read().await;
197 let connections = self.connections.read().await;
198
199 let success_rate = if nat_stats.total_attempts > 0 {
200 nat_stats.successful_connections as f64 / nat_stats.total_attempts as f64
201 } else {
202 0.0
203 };
204
205 let total_bytes: u64 = connections.values()
206 .map(|c| c.bytes_sent + c.bytes_received)
207 .sum();
208
209 let avg_rtt = if connections.is_empty() {
210 Duration::from_millis(0)
211 } else {
212 let total_rtt: Duration = connections.values()
213 .filter_map(|c| c.rtt)
214 .sum();
215 let count = connections.values().filter(|c| c.rtt.is_some()).count();
216 if count > 0 {
217 total_rtt / count as u32
218 } else {
219 Duration::from_millis(0)
220 }
221 };
222
223 let data_point = DataPoint {
224 timestamp: Instant::now(),
225 active_connections: connections.len(),
226 nat_success_rate: success_rate,
227 bytes_per_second: total_bytes,
228 avg_rtt,
229 };
230
231 let mut history = self.history.write().await;
232 if history.len() >= self.config.history_size {
233 history.pop_front();
234 }
235 history.push_back(data_point);
236 }
237
238 pub async fn render(&self) -> String {
240 self.record_data_point().await;
242
243 let mut output = String::new();
244
245 output.push_str("\x1B[2J\x1B[H");
247
248 output.push_str(&format!("{}🚀 ant-quic Connection Statistics Dashboard\n\n{}", terminal_ui::colors::BOLD, terminal_ui::colors::RESET));
250
251 let uptime = self.start_time.elapsed();
253 output.push_str(&format!("⏱️ Uptime: {}\n\n", format_duration(uptime)));
254
255 output.push_str(&self.render_overview_section().await);
257 output.push_str(&self.render_nat_section().await);
258 output.push_str(&self.render_connections_section().await);
259
260 if self.config.show_graphs {
261 output.push_str(&self.render_graphs_section().await);
262 }
263
264 output.push_str(&self.render_footer().await);
265
266 output
267 }
268
269 async fn render_overview_section(&self) -> String {
271 let node_stats = self.node_stats.read().await;
272 let _connections = self.connections.read().await;
273
274 let mut section = String::new();
275
276 section.push_str(&draw_box(
277 "📊 Overview",
278 &format!(
279 "Active Connections: {}\n\
280 Total Successful: {}\n\
281 Total Failed: {}\n\
282 Success Rate: {:.1}%",
283 format!("{}{}{}", terminal_ui::colors::GREEN, node_stats.active_connections, terminal_ui::colors::RESET),
284 node_stats.successful_connections,
285 node_stats.failed_connections,
286 if node_stats.successful_connections + node_stats.failed_connections > 0 {
287 (node_stats.successful_connections as f64 /
288 (node_stats.successful_connections + node_stats.failed_connections) as f64) * 100.0
289 } else {
290 0.0
291 }
292 ),
293 BoxStyle::Single,
294 50,
295 ));
296
297 section.push('\n');
298 section
299 }
300
301 async fn render_nat_section(&self) -> String {
303 let nat_stats = self.nat_stats.read().await;
304
305 let mut section = String::new();
306
307 section.push_str(&draw_box(
308 "🌐 NAT Traversal",
309 &format!(
310 "Total Attempts: {}\n\
311 Successful: {} ({:.1}%)\n\
312 Direct Connections: {}\n\
313 Relayed: {}\n\
314 Average Time: {:?}\n\
315 Active Sessions: {}",
316 nat_stats.total_attempts,
317 nat_stats.successful_connections,
318 if nat_stats.total_attempts > 0 {
319 (nat_stats.successful_connections as f64 / nat_stats.total_attempts as f64) * 100.0
320 } else {
321 0.0
322 },
323 nat_stats.direct_connections,
324 nat_stats.relayed_connections,
325 nat_stats.average_coordination_time,
326 nat_stats.active_sessions,
327 ),
328 BoxStyle::Single,
329 50,
330 ));
331
332 section.push('\n');
333 section
334 }
335
336 async fn render_connections_section(&self) -> String {
338 let connections = self.connections.read().await;
339
340 let mut section = String::new();
341
342 if connections.is_empty() {
343 section.push_str(&draw_box(
344 "🔗 Active Connections",
345 "No active connections",
346 BoxStyle::Single,
347 50,
348 ));
349 } else {
350 let mut content = String::new();
351 for (i, (peer_id, conn)) in connections.iter().enumerate() {
352 if i > 0 {
353 content.push_str("\n─────────────────────────────────────────────\n");
354 }
355
356 content.push_str(&format!(
357 "Peer: {}\n\
358 Address: {}\n\
359 Duration: {}\n\
360 Sent: {} | Received: {}\n\
361 RTT: {} | Loss: {:.1}%",
362 format!("{}{}{}", terminal_ui::colors::DIM, hex::encode(&peer_id.0[..8]), terminal_ui::colors::RESET),
363 conn.remote_address,
364 format_duration(conn.connected_at.elapsed()),
365 format_bytes(conn.bytes_sent),
366 format_bytes(conn.bytes_received),
367 conn.rtt.map(|d| format!("{:?}", d)).unwrap_or_else(|| "N/A".to_string()),
368 conn.packet_loss * 100.0,
369 ));
370 }
371
372 section.push_str(&draw_box(
373 &format!("🔗 Active Connections ({})", connections.len()),
374 &content,
375 BoxStyle::Single,
376 50,
377 ));
378 }
379
380 section.push('\n');
381 section
382 }
383
384 async fn render_graphs_section(&self) -> String {
386 let history = self.history.read().await;
387
388 if history.len() < 2 {
389 return String::new();
390 }
391
392 let mut section = String::new();
393
394 let conn_data: Vec<usize> = history.iter()
396 .map(|d| d.active_connections)
397 .collect();
398
399 section.push_str(&draw_box(
400 "📈 Connection History",
401 &render_mini_graph(&conn_data, 20, 50),
402 BoxStyle::Single,
403 50,
404 ));
405 section.push('\n');
406
407 let success_data: Vec<f64> = history.iter()
409 .map(|d| d.nat_success_rate * 100.0)
410 .collect();
411
412 section.push_str(&draw_box(
413 "📈 NAT Success Rate %",
414 &render_mini_graph_float(&success_data, 20, 50),
415 BoxStyle::Single,
416 50,
417 ));
418 section.push('\n');
419
420 section
421 }
422
423 async fn render_footer(&self) -> String {
425 let last_update = *self.last_update.read().await;
426
427 format!(
428 "\n{}\n{}",
429 format!("{}Last updated: {:?} ago{}", terminal_ui::colors::DIM, last_update.elapsed(), terminal_ui::colors::RESET),
430 format!("{}Press Ctrl+C to exit{}", terminal_ui::colors::DIM, terminal_ui::colors::RESET),
431 )
432 }
433}
434
435fn format_duration(duration: Duration) -> String {
437 let secs = duration.as_secs();
438 if secs < 60 {
439 format!("{}s", secs)
440 } else if secs < 3600 {
441 format!("{}m {}s", secs / 60, secs % 60)
442 } else {
443 format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
444 }
445}
446
447fn format_bytes(bytes: u64) -> String {
449 const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
450 let mut size = bytes as f64;
451 let mut unit_index = 0;
452
453 while size >= 1024.0 && unit_index < UNITS.len() - 1 {
454 size /= 1024.0;
455 unit_index += 1;
456 }
457
458 format!("{:.2} {}", size, UNITS[unit_index])
459}
460
461fn render_mini_graph(data: &[usize], height: usize, width: usize) -> String {
463 if data.is_empty() {
464 return "No data".to_string();
465 }
466
467 let max_val = *data.iter().max().unwrap_or(&1).max(&1) as f64;
468 let step = data.len().max(1) / width.min(data.len()).max(1);
469
470 let mut graph = vec![vec![' '; width]; height];
471
472 for (i, chunk) in data.chunks(step).enumerate() {
473 if i >= width {
474 break;
475 }
476
477 let avg = chunk.iter().sum::<usize>() as f64 / chunk.len() as f64;
478 let normalized = (avg / max_val * (height - 1) as f64).round() as usize;
479
480 for y in 0..=normalized {
481 let row = height - 1 - y;
482 graph[row][i] = '█';
483 }
484 }
485
486 let mut output = String::new();
487 for row in graph {
488 output.push_str(&row.iter().collect::<String>());
489 output.push('\n');
490 }
491
492 output.push_str(&format!("Max: {} | Latest: {}",
493 data.iter().max().unwrap_or(&0),
494 data.last().unwrap_or(&0)
495 ));
496
497 output
498}
499
500fn render_mini_graph_float(data: &[f64], height: usize, width: usize) -> String {
502 if data.is_empty() {
503 return "No data".to_string();
504 }
505
506 let max_val = data.iter().cloned().fold(f64::NEG_INFINITY, f64::max).max(1.0);
507 let step = data.len().max(1) / width.min(data.len()).max(1);
508
509 let mut graph = vec![vec![' '; width]; height];
510
511 for (i, chunk) in data.chunks(step).enumerate() {
512 if i >= width {
513 break;
514 }
515
516 let avg = chunk.iter().sum::<f64>() / chunk.len() as f64;
517 let normalized = (avg / max_val * (height - 1) as f64).round() as usize;
518
519 for y in 0..=normalized {
520 let row = height - 1 - y;
521 graph[row][i] = '█';
522 }
523 }
524
525 let mut output = String::new();
526 for row in graph {
527 output.push_str(&row.iter().collect::<String>());
528 output.push('\n');
529 }
530
531 output.push_str(&format!("Max: {:.1}% | Latest: {:.1}%",
532 data.iter().cloned().fold(f64::NEG_INFINITY, f64::max),
533 data.last().unwrap_or(&0.0)
534 ));
535
536 output
537}