"use client" import { useState, useEffect, useCallback, useRef } from "react" import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" import { Button } from "./ui/button" import { Badge } from "./ui/badge" import { Activity, TrendingDown, TrendingUp, Minus, RefreshCw, Wifi, FileText, Square } from "lucide-react" import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, LineChart, Line } from "recharts" import { useIsMobile } from "../hooks/use-mobile" import { fetchApi } from "@/lib/api-config" const TIMEFRAME_OPTIONS = [ { value: "hour", label: "1 Hour" }, { value: "6hour", label: "6 Hours" }, { value: "day", label: "24 Hours" }, { value: "3day", label: "3 Days" }, { value: "week", label: "7 Days" }, ] const TARGET_OPTIONS = [ { value: "gateway", label: "Gateway (Router)", realtime: false }, { value: "cloudflare", label: "Cloudflare (1.1.1.1)", realtime: true }, { value: "google", label: "Google DNS (8.8.8.8)", realtime: true }, ] // Realtime test configuration const REALTIME_TEST_DURATION = 120 // 2 minutes in seconds const REALTIME_TEST_INTERVAL = 5 // 5 seconds between tests interface LatencyHistoryPoint { timestamp: number value: number min?: number max?: number packet_loss?: number } interface LatencyStats { min: number max: number avg: number current: number } interface RealtimeResult { target: string target_ip: string latency_avg: number | null latency_min: number | null latency_max: number | null packet_loss: number status: string timestamp?: number } interface LatencyDetailModalProps { open: boolean onOpenChange: (open: boolean) => void currentLatency?: number } const CustomTooltip = ({ active, payload, label }: any) => { if (active && payload && payload.length) { const entry = payload[0] const packetLoss = entry?.payload?.packet_loss return (

{label}

Latency: {entry.value} ms
{packetLoss !== undefined && packetLoss > 0 && (
Pkt Loss: {packetLoss}%
)}
) } return null } const getStatusColor = (latency: number) => { if (latency >= 200) return "#ef4444" if (latency >= 100) return "#f59e0b" return "#22c55e" } const getStatusInfo = (latency: number | null) => { if (latency === null || latency === 0) return { status: "N/A", color: "bg-gray-500/10 text-gray-500 border-gray-500/20" } if (latency < 50) return { status: "Excellent", color: "bg-green-500/10 text-green-500 border-green-500/20" } if (latency < 100) return { status: "Good", color: "bg-green-500/10 text-green-500 border-green-500/20" } if (latency < 200) return { status: "Fair", color: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20" } return { status: "Poor", color: "bg-red-500/10 text-red-500 border-red-500/20" } } const getStatusText = (latency: number | null): string => { if (latency === null || latency === 0) return "N/A" if (latency < 50) return "Excellent" if (latency < 100) return "Good" if (latency < 200) return "Fair" return "Poor" } interface ReportData { target: string targetLabel: string isRealtime: boolean stats: LatencyStats realtimeResults: RealtimeResult[] data: LatencyHistoryPoint[] timeframe: string testDuration?: number } const generateLatencyReport = (report: ReportData) => { const now = new Date().toLocaleString() 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 statusText = report.isRealtime ? getStatusText(realtimeStats?.current ?? null) : getStatusText(report.stats.current) // Colors matching Lynis report const statusColorMap: Record = { "Excellent": "#16a34a", "Good": "#16a34a", "Fair": "#ca8a04", "Poor": "#dc2626", "N/A": "#64748b" } const statusColor = statusColorMap[statusText] || "#64748b" const timeframeLabel = TIMEFRAME_OPTIONS.find(t => t.value === report.timeframe)?.label || report.timeframe // Build test results table for realtime mode const realtimeTableRows = report.realtimeResults.map((r, i) => ` 0 ? ' class="warn"' : ''}> ${i + 1} ${new Date(r.timestamp || Date.now()).toLocaleTimeString()} ${r.latency_avg !== null ? r.latency_avg + ' ms' : 'Failed'} ${r.latency_min !== null ? r.latency_min + ' ms' : '-'} ${r.latency_max !== null ? r.latency_max + ' ms' : '-'} 0 ? ' style="color:#dc2626;font-weight:600;"' : ''}>${r.packet_loss}% ${getStatusText(r.latency_avg)} `).join('') // Build history summary for gateway mode const historyStats = report.data.length > 0 ? { samples: report.data.length, avgPacketLoss: (report.data.reduce((acc, d) => acc + (d.packet_loss || 0), 0) / report.data.length).toFixed(2), startTime: new Date(report.data[0].timestamp * 1000).toLocaleString(), endTime: new Date(report.data[report.data.length - 1].timestamp * 1000).toLocaleString(), } : null // Generate chart SVG const chartData = report.isRealtime ? report.realtimeResults.map(r => r.latency_avg || 0) : report.data.map(d => d.value || 0) let chartSvg = '

Not enough data points for chart

' if (chartData.length >= 2) { const minVal = Math.min(...chartData) const maxVal = Math.max(...chartData) const range = maxVal - minVal || 1 const width = 700 const height = 120 const padding = 30 const points = chartData.map((val, i) => { const x = padding + (i / (chartData.length - 1)) * (width - padding * 2) const y = height - padding - ((val - minVal) / range) * (height - padding * 2) return `${x},${y}` }).join(' ') const areaPoints = `${padding},${height - padding} ${points} ${width - padding},${height - padding}` chartSvg = ` ${maxVal.toFixed(0)}ms ${((minVal + maxVal) / 2).toFixed(0)}ms ${minVal.toFixed(0)}ms ${chartData.length} samples ` } const html = ` Network Latency Report - ${report.targetLabel}
ProxMenux Network Latency Report Review the report, then print or save as PDF
⌘P / Ctrl+P
ProxMenux

Network Latency Report

ProxMenux Monitor - Network Performance Analysis

Date: ${now}
Target: ${report.targetLabel}
Mode: ${report.isRealtime ? 'Real-time Test' : 'Historical Analysis'}
ID: PMXL-${Date.now().toString(36).toUpperCase()}
1. Executive Summary
0 300+
${report.isRealtime ? (realtimeStats?.avg?.toFixed(0) ?? 'N/A') : report.stats.avg} ms
${statusText}

Network Latency Assessment${report.isRealtime ? ' (Real-time)' : ''}

${report.isRealtime ? `Real-time latency test to ${report.targetLabel} with ${report.realtimeResults.length} samples collected over ${report.testDuration ? Math.round(report.testDuration / 60) + ' minute(s)' : 'the test period'}. Average latency: ${realtimeStats?.avg?.toFixed(1) ?? 'N/A'} ms. ${realtimeStats && realtimeStats.avgPacketLoss > 0 ? `Average packet loss: ${realtimeStats.avgPacketLoss.toFixed(1)}%.` : 'No packet loss detected.'}` : `Historical latency analysis to Gateway over ${timeframeLabel.toLowerCase()}. ${report.data.length} samples analyzed. Average latency: ${report.stats.avg} ms.` }

Minimum ${report.isRealtime ? (realtimeStats?.min?.toFixed(1) ?? 'N/A') : report.stats.min} ms
Average ${report.isRealtime ? (realtimeStats?.avg?.toFixed(1) ?? 'N/A') : report.stats.avg} ms
Maximum ${report.isRealtime ? (realtimeStats?.max?.toFixed(1) ?? 'N/A') : report.stats.max} ms
2. Latency Statistics
${report.isRealtime ? (realtimeStats?.current?.toFixed(1) ?? 'N/A') : report.stats.current} ms
Current
${report.isRealtime ? (realtimeStats?.min?.toFixed(1) ?? 'N/A') : report.stats.min} ms
Minimum
${report.isRealtime ? (realtimeStats?.avg?.toFixed(1) ?? 'N/A') : report.stats.avg} ms
Average
${report.isRealtime ? (realtimeStats?.max?.toFixed(1) ?? 'N/A') : report.stats.max} ms
Maximum
Sample Count
${report.isRealtime ? report.realtimeResults.length : report.data.length}
Packet Loss (Avg)
${report.isRealtime ? (realtimeStats?.avgPacketLoss?.toFixed(1) ?? '0') : (historyStats?.avgPacketLoss ?? '0')}%
Test Period
${report.isRealtime ? (report.testDuration ? Math.round(report.testDuration / 60) + ' min' : 'Real-time') : timeframeLabel}
${report.isRealtime && report.realtimeResults.length > 0 ? `
3. Detailed Test Results
${realtimeTableRows}
# Time Latency (Avg) Min Max Packet Loss Status
` : ''}
${report.isRealtime ? '4' : '3'}. Latency Graph
${chartSvg}
${report.isRealtime ? '5' : '4'}. Performance Thresholds

Excellent (< 50ms): Optimal for real-time applications, gaming, and video calls.

Good (50-100ms): Acceptable for most applications with minimal impact.

Fair (100-200ms): Noticeable delay. May affect VoIP and interactive applications.

Poor (> 200ms): Significant latency. Investigation recommended.

${report.isRealtime ? '6' : '5'}. Methodology
Test Method
ICMP Echo Request (Ping)
Samples per Test
3 consecutive pings
Target
${report.targetLabel}
Target IP
${report.target === 'gateway' ? 'Default Gateway' : report.target === 'cloudflare' ? '1.1.1.1' : '8.8.8.8'}

Performance Assessment

${ statusText === 'Excellent' ? 'Network latency is excellent. No action required.' : statusText === 'Good' ? 'Network latency is within acceptable parameters.' : statusText === 'Fair' ? 'Network latency is elevated. Consider investigating network congestion or routing issues.' : statusText === 'Poor' ? 'Network latency is critically high. Immediate investigation recommended.' : 'Unable to determine network status.' }

Samples per Test
3 consecutive pings
Target
${report.targetLabel}
Target IP
${report.target === 'gateway' ? 'Default Gateway' : report.target === 'cloudflare' ? '1.1.1.1' : '8.8.8.8'}

Performance Rating

${ statusText === 'Excellent' ? 'Network latency is excellent. No action required.' : statusText === 'Good' ? 'Network latency is within acceptable parameters.' : statusText === 'Fair' ? 'Network latency is elevated. Consider investigating network congestion or routing issues.' : statusText === 'Poor' ? 'Network latency is critically high. Immediate investigation recommended.' : 'Unable to determine network status.' }

` const printWindow = window.open('', '_blank') if (printWindow) { printWindow.document.write(html) printWindow.document.close() } } export function LatencyDetailModal({ open, onOpenChange, currentLatency }: LatencyDetailModalProps) { const [timeframe, setTimeframe] = useState("hour") const [target, setTarget] = useState("gateway") const [data, setData] = useState([]) const [stats, setStats] = useState({ min: 0, max: 0, avg: 0, current: 0 }) const [loading, setLoading] = useState(true) const [realtimeResults, setRealtimeResults] = useState([]) const [realtimeTesting, setRealtimeTesting] = useState(false) const [testProgress, setTestProgress] = useState(0) // 0-100 percentage const [testStartTime, setTestStartTime] = useState(null) const testIntervalRef = useRef(null) const isMobile = useIsMobile() const isRealtime = TARGET_OPTIONS.find(t => t.value === target)?.realtime ?? false // Cleanup on unmount or close useEffect(() => { if (!open) { stopRealtimeTest() } return () => { stopRealtimeTest() } }, [open]) // Fetch history for gateway useEffect(() => { if (open && target === "gateway") { fetchHistory() } }, [open, timeframe, target]) // Auto-start test when switching to realtime target useEffect(() => { if (open && isRealtime) { // Clear previous results and start new test setRealtimeResults([]) startRealtimeTest() } else { stopRealtimeTest() } }, [open, target]) const fetchHistory = async () => { setLoading(true) try { const result = await fetchApi<{ data: LatencyHistoryPoint[]; stats: LatencyStats; target: string }>( `/api/network/latency/history?target=${target}&timeframe=${timeframe}` ) if (result && result.data) { setData(result.data) setStats(result.stats) } } catch (err) { // Silently fail } finally { setLoading(false) } } const runSingleTest = useCallback(async () => { try { const result = await fetchApi(`/api/network/latency/current?target=${target}`) if (result) { const resultWithTimestamp = { ...result, timestamp: Date.now() } setRealtimeResults(prev => [...prev, resultWithTimestamp]) } } catch (err) { // Silently fail } }, [target]) const startRealtimeTest = useCallback(() => { if (realtimeTesting) return setRealtimeTesting(true) setTestProgress(0) setTestStartTime(Date.now()) // Run first test immediately runSingleTest() // Set up interval for subsequent tests const totalTests = REALTIME_TEST_DURATION / REALTIME_TEST_INTERVAL let testCount = 1 testIntervalRef.current = setInterval(() => { testCount++ const progress = Math.min(100, (testCount / totalTests) * 100) setTestProgress(progress) runSingleTest() // Stop after duration if (testCount >= totalTests) { stopRealtimeTest() } }, REALTIME_TEST_INTERVAL * 1000) }, [realtimeTesting, runSingleTest]) const stopRealtimeTest = useCallback(() => { if (testIntervalRef.current) { clearInterval(testIntervalRef.current) testIntervalRef.current = null } setRealtimeTesting(false) setTestProgress(100) }, []) const restartRealtimeTest = useCallback(() => { // Don't clear results - add to existing data startRealtimeTest() }, [startRealtimeTest]) // Format chart data const chartData = data.map(point => ({ ...point, time: new Date(point.timestamp * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), })) const realtimeChartData = realtimeResults.map((r, i) => ({ time: new Date(r.timestamp || Date.now()).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }), value: r.latency_avg, 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 displayStats = isRealtime ? { current: realtimeStats?.current ?? 0, min: realtimeStats?.min ?? 0, max: realtimeStats?.max ?? 0, avg: Math.round((realtimeStats?.avg ?? 0) * 10) / 10, } : stats const statusInfo = getStatusInfo(displayStats.current) // Calculate test duration for report based on first and last result timestamps const testDuration = realtimeResults.length >= 2 ? Math.round(((realtimeResults[realtimeResults.length - 1].timestamp || Date.now()) - (realtimeResults[0].timestamp || Date.now())) / 1000) : realtimeResults.length === 1 ? 5 // Single sample = 5 seconds (one test) : 0 return (
Network Latency
{!isRealtime && ( )} {isRealtime && ( <> {realtimeTesting ? ( ) : ( )} )}
{/* Progress bar for realtime test */} {isRealtime && realtimeTesting && (
Testing... {Math.round(testProgress)}% {Math.round((REALTIME_TEST_DURATION * (1 - testProgress / 100)))}s remaining
)} {/* Stats Cards */}
Current
{displayStats.current || '-'} ms
Min
{displayStats.min || '-'} ms
Avg
{displayStats.avg || '-'} ms
Max
{displayStats.max || '-'} ms
{/* Status Badge */}
{statusInfo.status} {isRealtime && ( {realtimeResults.length} sample{realtimeResults.length !== 1 ? 's' : ''} collected {realtimeStats?.packetLoss ? ` | ${realtimeStats.packetLoss}% packet loss` : ''} )}
{/* Chart */}
{isRealtime ? ( realtimeChartData.length > 0 ? ( `${v}ms`} /> } /> ) : (

{realtimeTesting ? 'Collecting data...' : 'No data yet. Click "Test Again" to start.'}

) ) : loading ? (
) : chartData.length > 0 ? ( `${v}ms`} /> } /> ) : (

No latency data available for this period

Data is collected every 60 seconds

)}
{/* Info for realtime mode */} {isRealtime && (

Real-time Mode: Tests run for 2 minutes with readings every 5 seconds. Click "Test Again" to add more samples. All data is included in the report.

)}
) }