diff --git a/AppImage/components/latency-detail-modal.tsx b/AppImage/components/latency-detail-modal.tsx index e55c5111..86159a47 100644 --- a/AppImage/components/latency-detail-modal.tsx +++ b/AppImage/components/latency-detail-modal.tsx @@ -125,13 +125,20 @@ const generateLatencyReport = (report: ReportData) => { const logoUrl = `${window.location.origin}/images/proxmenux-logo.png` // Calculate stats for realtime results - const realtimeStats = report.realtimeResults.length > 0 ? { - min: Math.min(...report.realtimeResults.filter(r => r.latency_min !== null).map(r => r.latency_min!)), - max: Math.max(...report.realtimeResults.filter(r => r.latency_max !== null).map(r => r.latency_max!)), - avg: report.realtimeResults.reduce((acc, r) => acc + (r.latency_avg || 0), 0) / report.realtimeResults.length, - current: report.realtimeResults[report.realtimeResults.length - 1]?.latency_avg ?? null, - avgPacketLoss: report.realtimeResults.reduce((acc, r) => acc + (r.packet_loss || 0), 0) / report.realtimeResults.length, - } : null + const realtimeStats = report.realtimeResults.length > 0 ? (() => { + const validResults = report.realtimeResults.filter(r => r.latency_avg !== null) + const minValues = report.realtimeResults.filter(r => r.latency_min !== null).map(r => r.latency_min!) + const maxValues = report.realtimeResults.filter(r => r.latency_max !== null).map(r => r.latency_max!) + const avgValues = validResults.map(r => r.latency_avg!) + + return { + min: minValues.length > 0 ? Math.min(...minValues) : (avgValues.length > 0 ? Math.min(...avgValues) : 0), + max: maxValues.length > 0 ? Math.max(...maxValues) : (avgValues.length > 0 ? Math.max(...avgValues) : 0), + avg: validResults.length > 0 ? validResults.reduce((acc, r) => acc + (r.latency_avg || 0), 0) / validResults.length : 0, + current: report.realtimeResults[report.realtimeResults.length - 1]?.latency_avg ?? null, + avgPacketLoss: report.realtimeResults.reduce((acc, r) => acc + (r.packet_loss || 0), 0) / report.realtimeResults.length, + } + })() : null const statusText = report.isRealtime ? getStatusText(realtimeStats?.current ?? null) @@ -170,17 +177,11 @@ const generateLatencyReport = (report: ReportData) => { endTime: new Date(report.data[report.data.length - 1].timestamp * 1000).toLocaleString(), } : null - // Generate chart SVG - expand realtime to all 3 values (min, avg, max) per sample + // Generate chart SVG - use average values for the line chart const chartData = report.isRealtime - ? report.realtimeResults.flatMap(r => { - const points: number[] = [] - if (r.latency_min !== null) points.push(r.latency_min) - if (r.latency_avg !== null && r.latency_avg !== r.latency_min && r.latency_avg !== r.latency_max) { - points.push(r.latency_avg) - } - if (r.latency_max !== null) points.push(r.latency_max) - return points.length > 0 ? points : [r.latency_avg ?? 0] - }) + ? report.realtimeResults + .filter(r => r.latency_avg !== null) + .map(r => r.latency_avg!) : report.data.map(d => d.value || 0) let chartSvg = '
Not enough data points for chart
' @@ -779,30 +780,32 @@ export function LatencyDetailModal({ open, onOpenChange, currentLatency }: Laten time: new Date(point.timestamp * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), })) - // Expand each sample to 3 data points (min, avg, max) for accurate representation - const realtimeChartData = realtimeResults.flatMap((r, i) => { - const time = new Date(r.timestamp || Date.now()).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }) - const points = [] - if (r.latency_min !== null) points.push({ time, value: r.latency_min, packet_loss: r.packet_loss }) - if (r.latency_avg !== null && r.latency_avg !== r.latency_min && r.latency_avg !== r.latency_max) { - points.push({ time, value: r.latency_avg, packet_loss: r.packet_loss }) - } - if (r.latency_max !== null) points.push({ time, value: r.latency_max, packet_loss: r.packet_loss }) - // If no valid points, add avg as fallback - if (points.length === 0 && r.latency_avg !== null) { - points.push({ time, value: r.latency_avg, packet_loss: r.packet_loss }) - } - return points - }) + // Use average value for the chart line (min/max are shown in stats) + const realtimeChartData = realtimeResults + .filter(r => r.latency_avg !== null) + .map(r => ({ + time: new Date(r.timestamp || Date.now()).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }), + value: r.latency_avg, + min: r.latency_min, + max: r.latency_max, + packet_loss: r.packet_loss + })) // Calculate realtime stats - const realtimeStats = realtimeResults.length > 0 ? { - current: realtimeResults[realtimeResults.length - 1]?.latency_avg ?? 0, - min: Math.min(...realtimeResults.filter(r => r.latency_min !== null).map(r => r.latency_min!)) || 0, - max: Math.max(...realtimeResults.filter(r => r.latency_max !== null).map(r => r.latency_max!)) || 0, - avg: realtimeResults.reduce((acc, r) => acc + (r.latency_avg || 0), 0) / realtimeResults.length, - packetLoss: realtimeResults[realtimeResults.length - 1]?.packet_loss ?? 0, - } : null + const realtimeStats = realtimeResults.length > 0 ? (() => { + const validResults = realtimeResults.filter(r => r.latency_avg !== null) + const minValues = realtimeResults.filter(r => r.latency_min !== null).map(r => r.latency_min!) + const maxValues = realtimeResults.filter(r => r.latency_max !== null).map(r => r.latency_max!) + const avgValues = validResults.map(r => r.latency_avg!) + + return { + current: realtimeResults[realtimeResults.length - 1]?.latency_avg ?? 0, + min: minValues.length > 0 ? Math.min(...minValues) : (avgValues.length > 0 ? Math.min(...avgValues) : 0), + max: maxValues.length > 0 ? Math.max(...maxValues) : (avgValues.length > 0 ? Math.max(...avgValues) : 0), + avg: validResults.length > 0 ? validResults.reduce((acc, r) => acc + (r.latency_avg || 0), 0) / validResults.length : 0, + packetLoss: realtimeResults[realtimeResults.length - 1]?.packet_loss ?? 0, + } + })() : null const displayStats = isRealtime ? { current: realtimeStats?.current ?? 0, diff --git a/AppImage/scripts/flask_server.py b/AppImage/scripts/flask_server.py index 9b905390..24dafeaa 100644 --- a/AppImage/scripts/flask_server.py +++ b/AppImage/scripts/flask_server.py @@ -631,34 +631,36 @@ def init_latency_db(): return False def _measure_latency(target_ip: str) -> dict: - """Ping a target and return latency stats. Uses 3 pings and returns the average to avoid false positives.""" - try: - result = subprocess.run( - ['ping', '-c', '3', '-W', '2', target_ip], - capture_output=True, text=True, timeout=10 - ) - - if result.returncode == 0: - latencies = [] - for line in result.stdout.split('\n'): - if 'time=' in line: - try: - latency_str = line.split('time=')[1].split()[0] - latencies.append(float(latency_str)) - except: - pass - - if latencies: - return { - 'success': True, - 'avg': round(sum(latencies) / len(latencies), 1), - 'packet_loss': round((3 - len(latencies)) / 3 * 100, 1) - } - - # Ping failed - 100% packet loss - return {'success': False, 'avg': None, 'packet_loss': 100.0} - except Exception: - return {'success': False, 'avg': None, 'packet_loss': 100.0} + """Ping a target and return latency stats. Uses 3 pings and returns avg, min, max for full visibility.""" + try: + result = subprocess.run( + ['ping', '-c', '3', '-W', '2', target_ip], + capture_output=True, text=True, timeout=10 + ) + + if result.returncode == 0: + latencies = [] + for line in result.stdout.split('\n'): + if 'time=' in line: + try: + latency_str = line.split('time=')[1].split()[0] + latencies.append(float(latency_str)) + except: + pass + + if latencies: + return { + 'success': True, + 'avg': round(sum(latencies) / len(latencies), 1), + 'min': round(min(latencies), 1), + 'max': round(max(latencies), 1), + 'packet_loss': round((3 - len(latencies)) / 3 * 100, 1) + } + + # Ping failed - 100% packet loss + return {'success': False, 'avg': None, 'min': None, 'max': None, 'packet_loss': 100.0} + except Exception: + return {'success': False, 'avg': None, 'min': None, 'max': None, 'packet_loss': 100.0} def _record_latency(): """Record latency to the default gateway. Only stores the average of 3 pings.""" @@ -807,19 +809,21 @@ def get_current_latency(target='gateway'): # Fallback: do fresh measurement if no stored data stats = _measure_latency(target_ip) else: - # Cloudflare/Google: fresh ping (not continuously monitored) - target_ip = LATENCY_TARGETS.get(target, target) - stats = _measure_latency(target_ip) - - return { - 'target': target, - 'target_ip': target_ip, - 'latency_avg': stats['avg'], - 'packet_loss': stats['packet_loss'], - 'status': 'ok' if stats['success'] and stats['avg'] and stats['avg'] < 100 else 'warning' if stats['success'] else 'error' - } - except Exception: - return {'target': target, 'latency_avg': None, 'status': 'error'} + # Cloudflare/Google: fresh ping (not continuously monitored) + target_ip = LATENCY_TARGETS.get(target, target) + stats = _measure_latency(target_ip) + + return { + 'target': target, + 'target_ip': target_ip, + 'latency_avg': stats['avg'], + 'latency_min': stats.get('min'), + 'latency_max': stats.get('max'), + 'packet_loss': stats['packet_loss'], + 'status': 'ok' if stats['success'] and stats['avg'] and stats['avg'] < 100 else 'warning' if stats['success'] else 'error' + } + except Exception: + return {'target': target, 'latency_avg': None, 'latency_min': None, 'latency_max': None, 'status': 'error'} def _health_collector_loop():