asupersync 0.3.4

Spec-first, cancel-correct, capability-secure async runtime for Rust.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
//! ATP relay telemetry HTTP dashboard for real-time monitoring.
//!
//! Provides HTTP endpoints for relay operators to monitor connection health,
//! throughput, error rates, and performance metrics without exposing sensitive
//! transfer data.

use crate::atp::telemetry::{RelayDashboardData, RelayTelemetryCollector};
use crate::net::atp::relay::RelayService;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use serde_json;

/// HTTP dashboard for ATP relay telemetry.
pub struct RelayDashboard {
    /// Telemetry collector with accumulated metrics
    telemetry: Arc<Mutex<RelayTelemetryCollector>>,
    /// Last collection time for rate limiting
    last_collection: Arc<Mutex<Instant>>,
    /// Minimum interval between collections to avoid overhead
    collection_interval: Duration,
}

impl RelayDashboard {
    /// Create new relay dashboard.
    pub fn new() -> Self {
        Self {
            telemetry: Arc::new(Mutex::new(RelayTelemetryCollector::new())),
            last_collection: Arc::new(Mutex::new(Instant::now())),
            collection_interval: Duration::from_secs(5), // Collect every 5 seconds
        }
    }

    /// Update telemetry from relay service (called periodically).
    pub fn update_from_service(&self, service: &RelayService) -> Result<(), String> {
        let mut last_collection = self.last_collection.lock().map_err(|_| "Lock poisoned")?;

        // Rate limit collections to avoid performance impact
        if last_collection.elapsed() < self.collection_interval {
            return Ok(());
        }

        let mut telemetry = self.telemetry.lock().map_err(|_| "Lock poisoned")?;
        telemetry.collect_from_service(service);
        *last_collection = Instant::now();

        Ok(())
    }

    /// Get current dashboard data as JSON.
    pub fn get_dashboard_json(&self) -> Result<String, String> {
        let mut telemetry = self.telemetry.lock().map_err(|_| "Lock poisoned")?;
        let dashboard_data = telemetry.get_dashboard_data();

        serde_json::to_string_pretty(&dashboard_data)
            .map_err(|e| format!("JSON serialization failed: {}", e))
    }

    /// Get telemetry summary in plain text format.
    pub fn get_summary_text(&self) -> Result<String, String> {
        let telemetry = self.telemetry.lock().map_err(|_| "Lock poisoned")?;
        let data = telemetry.get_dashboard_data();
        let current = &data.current;

        let summary = format!(
            r#"ATP Relay Telemetry Summary
============================

Connection Status:
- Active Reservations: {}
- Total Reservations: {}
- Total Packets Forwarded: {}
- Total Bytes Forwarded: {} MB
- Recent Forward Rate: {} packets/period

Transport Breakdown:
UDP Transport:
- Packets: {} ({:.1}%)
- Bytes: {} MB
- Avg Latency: {} μs
- Min/Max Latency: {}/{} μs

TCP/TLS 443 Transport:
- Packets: {} ({:.1}%)
- Bytes: {} MB
- Avg Latency: {} μs
- Min/Max Latency: {}/{} μs

Error Metrics:
- Quota Rejections: {}
- Auth Rejections: {}
- Bytes Rejected: {} MB
- Recent Error Rate: {} errors/period

Historical Data Points: {}
"#,
            current.connections.active_reservations,
            current.connections.total_reservations,
            current.connections.total_packets_forwarded,
            current.connections.total_bytes_forwarded / 1_048_576, // Convert to MB
            current.connections.recent_forward_rate,

            current.transport_udp.packets_forwarded,
            if current.connections.total_packets_forwarded > 0 {
                (current.transport_udp.packets_forwarded as f64 / current.connections.total_packets_forwarded as f64) * 100.0
            } else { 0.0 },
            current.transport_udp.bytes_forwarded / 1_048_576,
            current.transport_udp.average_latency_micros,
            current.transport_udp.min_latency_micros,
            current.transport_udp.max_latency_micros,

            current.transport_tcp_tls.packets_forwarded,
            if current.connections.total_packets_forwarded > 0 {
                (current.transport_tcp_tls.packets_forwarded as f64 / current.connections.total_packets_forwarded as f64) * 100.0
            } else { 0.0 },
            current.transport_tcp_tls.bytes_forwarded / 1_048_576,
            current.transport_tcp_tls.average_latency_micros,
            current.transport_tcp_tls.min_latency_micros,
            current.transport_tcp_tls.max_latency_micros,

            current.errors.quota_rejections,
            current.errors.auth_rejections,
            current.errors.bytes_rejected / 1_048_576,
            current.errors.recent_error_rate,

            data.historical.len()
        );

        Ok(summary)
    }

    /// Get HTML dashboard page.
    pub fn get_dashboard_html(&self) -> Result<String, String> {
        let json_data = self.get_dashboard_json()?;

        let html = format!(r#"<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ATP Relay Telemetry Dashboard</title>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <style>
        body {{
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            margin: 0;
            padding: 20px;
            background: #f5f5f5;
        }}
        .container {{ max-width: 1200px; margin: 0 auto; }}
        h1 {{ color: #2c3e50; text-align: center; }}
        .metrics-grid {{
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
            gap: 20px;
            margin: 20px 0;
        }}
        .metric-card {{
            background: white;
            border-radius: 8px;
            padding: 20px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }}
        .metric-title {{ font-weight: bold; color: #34495e; margin-bottom: 10px; }}
        .metric-value {{ font-size: 24px; color: #3498db; }}
        .metric-unit {{ font-size: 14px; color: #7f8c8d; }}
        .chart-container {{
            position: relative;
            height: 300px;
            margin: 20px 0;
        }}
        .status-good {{ color: #27ae60; }}
        .status-warning {{ color: #f39c12; }}
        .status-error {{ color: #e74c3c; }}
        .refresh-btn {{
            background: #3498db;
            color: white;
            border: none;
            padding: 10px 20px;
            border-radius: 4px;
            cursor: pointer;
            margin: 10px 0;
        }}
        .refresh-btn:hover {{ background: #2980b9; }}
    </style>
</head>
<body>
    <div class="container">
        <h1>ATP Relay Telemetry Dashboard</h1>
        <button class="refresh-btn" onclick="refreshData()">Refresh Data</button>
        <div id="lastUpdate">Last Updated: <span id="timestamp"></span></div>

        <div class="metrics-grid">
            <div class="metric-card">
                <div class="metric-title">Active Reservations</div>
                <div class="metric-value" id="activeReservations">-</div>
            </div>
            <div class="metric-card">
                <div class="metric-title">Total Packets Forwarded</div>
                <div class="metric-value" id="totalPackets">-</div>
            </div>
            <div class="metric-card">
                <div class="metric-title">Total Bytes Forwarded</div>
                <div class="metric-value" id="totalBytes">-</div>
                <div class="metric-unit">MB</div>
            </div>
            <div class="metric-card">
                <div class="metric-title">Error Rate</div>
                <div class="metric-value" id="errorRate">-</div>
                <div class="metric-unit">errors/period</div>
            </div>
        </div>

        <div class="metrics-grid">
            <div class="metric-card">
                <div class="metric-title">UDP Transport</div>
                <div>Packets: <span id="udpPackets">-</span></div>
                <div>Avg Latency: <span id="udpLatency">-</span> μs</div>
            </div>
            <div class="metric-card">
                <div class="metric-title">TCP/TLS Transport</div>
                <div>Packets: <span id="tcpPackets">-</span></div>
                <div>Avg Latency: <span id="tcpLatency">-</span> μs</div>
            </div>
        </div>

        <div class="metric-card">
            <div class="metric-title">Throughput Over Time</div>
            <div class="chart-container">
                <canvas id="throughputChart"></canvas>
            </div>
        </div>
    </div>

    <script>
        let telemetryData = {};
        let throughputChart;

        function initChart() {{
            const ctx = document.getElementById('throughputChart').getContext('2d');
            throughputChart = new Chart(ctx, {{
                type: 'line',
                data: {{
                    labels: [],
                    datasets: [{{
                        label: 'Packets/Period',
                        data: [],
                        borderColor: '#3498db',
                        backgroundColor: 'rgba(52, 152, 219, 0.1)',
                        tension: 0.1
                    }}]
                }},
                options: {{
                    responsive: true,
                    maintainAspectRatio: false,
                    scales: {{
                        y: {{
                            beginAtZero: true
                        }}
                    }}
                }}
            }});
        }}

        function updateMetrics() {{
            if (!telemetryData.current) return;

            const current = telemetryData.current;

            document.getElementById('activeReservations').textContent = current.connections.active_reservations;
            document.getElementById('totalPackets').textContent = current.connections.total_packets_forwarded.toLocaleString();
            document.getElementById('totalBytes').textContent = Math.round(current.connections.total_bytes_forwarded / 1048576);
            document.getElementById('errorRate').textContent = current.errors.recent_error_rate;

            document.getElementById('udpPackets').textContent = current.transport_udp.packets_forwarded.toLocaleString();
            document.getElementById('udpLatency').textContent = current.transport_udp.average_latency_micros;
            document.getElementById('tcpPackets').textContent = current.transport_tcp_tls.packets_forwarded.toLocaleString();
            document.getElementById('tcpLatency').textContent = current.transport_tcp_tls.average_latency_micros;

            // Update timestamp
            const timestamp = new Date(current.timestamp_micros / 1000);
            document.getElementById('timestamp').textContent = timestamp.toLocaleString();

            // Update chart with historical data
            if (telemetryData.historical && throughputChart) {{
                const labels = telemetryData.historical.map(snapshot => {{
                    const date = new Date(snapshot.timestamp_micros / 1000);
                    return date.toLocaleTimeString();
                }});
                const data = telemetryData.historical.map(snapshot => snapshot.connections.recent_forward_rate);

                throughputChart.data.labels = labels;
                throughputChart.data.datasets[0].data = data;
                throughputChart.update();
            }}
        }}

        async function refreshData() {{
            try {{
                // In a real implementation, this would fetch from /relay/telemetry endpoint
                // For now, we'll use the embedded data
                telemetryData = {json_data};
                updateMetrics();
            }} catch (error) {{
                console.error('Failed to refresh data:', error);
            }}
        }}

        // Initialize
        window.onload = function() {{
            initChart();
            telemetryData = {json_data};
            updateMetrics();
        }};
    </script>
</body>
</html>"#, json_data = json_data, json_data = json_data);

        Ok(html)
    }

    /// Create a new telemetry collector reference for recording events.
    pub fn telemetry_collector(&self) -> Arc<Mutex<RelayTelemetryCollector>> {
        Arc::clone(&self.telemetry)
    }
}

impl Default for RelayDashboard {
    fn default() -> Self {
        Self::new()
    }
}

/// HTTP handler functions for relay telemetry endpoints.
pub mod handlers {
    use super::*;

    /// Handle /relay/telemetry endpoint - returns JSON data.
    pub fn handle_telemetry_json(dashboard: &RelayDashboard) -> Result<String, String> {
        dashboard.get_dashboard_json()
    }

    /// Handle /relay/dashboard endpoint - returns HTML dashboard.
    pub fn handle_dashboard_html(dashboard: &RelayDashboard) -> Result<String, String> {
        dashboard.get_dashboard_html()
    }

    /// Handle /relay/status endpoint - returns plain text summary.
    pub fn handle_status_text(dashboard: &RelayDashboard) -> Result<String, String> {
        dashboard.get_summary_text()
    }

    /// Health check endpoint for monitoring systems.
    pub fn handle_health_check(dashboard: &RelayDashboard) -> Result<String, String> {
        let telemetry = dashboard.telemetry.lock().map_err(|_| "Lock poisoned")?;
        let data = telemetry.get_dashboard_data();

        // Simple health check based on error rates
        let total_errors = data.current.errors.quota_rejections + data.current.errors.auth_rejections;
        let total_packets = data.current.connections.total_packets_forwarded;

        let health_status = if total_packets == 0 {
            "STARTING"
        } else if total_errors == 0 {
            "HEALTHY"
        } else {
            let error_rate = (total_errors as f64 / total_packets as f64) * 100.0;
            if error_rate < 1.0 {
                "HEALTHY"
            } else if error_rate < 5.0 {
                "WARNING"
            } else {
                "UNHEALTHY"
            }
        };

        Ok(format!(
            "ATP Relay Health: {}\nActive Reservations: {}\nTotal Packets: {}\nTotal Errors: {}",
            health_status,
            data.current.connections.active_reservations,
            total_packets,
            total_errors
        ))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::net::atp::relay::RelayServiceConfig;

    #[test]
    fn dashboard_creates_with_telemetry_collector() {
        let dashboard = RelayDashboard::new();
        let collector = dashboard.telemetry_collector();

        // Verify we can get a lock on the collector
        assert!(collector.lock().is_ok());
    }

    #[test]
    fn dashboard_generates_json_data() {
        let dashboard = RelayDashboard::new();

        let json = dashboard.get_dashboard_json().expect("Should generate JSON");
        assert!(json.contains("current"));
        assert!(json.contains("historical"));
        assert!(json.contains("timestamp_micros"));
    }

    #[test]
    fn dashboard_generates_html() {
        let dashboard = RelayDashboard::new();

        let html = dashboard.get_dashboard_html().expect("Should generate HTML");
        assert!(html.contains("<!DOCTYPE html>"));
        assert!(html.contains("ATP Relay Telemetry Dashboard"));
        assert!(html.contains("throughputChart"));
    }

    #[test]
    fn dashboard_generates_text_summary() {
        let dashboard = RelayDashboard::new();

        let summary = dashboard.get_summary_text().expect("Should generate summary");
        assert!(summary.contains("ATP Relay Telemetry Summary"));
        assert!(summary.contains("Connection Status"));
        assert!(summary.contains("Transport Breakdown"));
        assert!(summary.contains("Error Metrics"));
    }

    #[test]
    fn dashboard_update_from_service_rate_limits() {
        let dashboard = RelayDashboard::new();
        let service = RelayService::new(RelayServiceConfig::default());

        // First update should succeed
        assert!(dashboard.update_from_service(&service).is_ok());

        // Immediate second update should be skipped due to rate limiting
        assert!(dashboard.update_from_service(&service).is_ok());
    }

    #[test]
    fn health_check_reports_status() {
        let dashboard = RelayDashboard::new();

        let health = handlers::handle_health_check(&dashboard).expect("Should generate health");
        assert!(health.contains("ATP Relay Health: STARTING"));
        assert!(health.contains("Active Reservations: 0"));
    }
}