"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)", shortLabel: "Gateway", realtime: false }, { value: "cloudflare", label: "Cloudflare (1.1.1.1)", shortLabel: "Cloudflare", realtime: true }, { value: "google", label: "Google DNS (8.8.8.8)", shortLabel: "Google DNS", 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 data = entry?.payload const packetLoss = data?.packet_loss const hasMinMax = data?.min !== undefined && data?.max !== undefined && data?.min !== data?.max return (

{label}

{hasMinMax ? ( // Show min/avg/max when downsampled data is available <>
Min: {data.min} ms
Avg: {data.value} ms
Max: {data.max} ms
) : ( // Simple latency display for single data points
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 - all values are individual ping measurements in latency_avg const validRealtimeValues = report.realtimeResults.filter(r => r.latency_avg !== null).map(r => r.latency_avg!) const realtimeStats = validRealtimeValues.length > 0 ? { min: Math.min(...validRealtimeValues), max: Math.max(...validRealtimeValues), avg: validRealtimeValues.reduce((acc, v) => acc + v, 0) / validRealtimeValues.length, current: validRealtimeValues[validRealtimeValues.length - 1] ?? 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 - each row is now an individual ping measurement const realtimeTableRows = report.realtimeResults.map((r, i) => ` 0 ? ' class="warn"' : ''}> ${i + 1} ${new Date(r.timestamp || Date.now()).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })} ${r.latency_avg !== null ? r.latency_avg.toFixed(1) + ' ms' : 'Failed'} 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 // Build history table rows for gateway mode (last 20 records) const historyTableRows = report.data.slice(-20).map((d, i) => ` 0 ? ' class="warn"' : ''}> ${i + 1} ${new Date(d.timestamp * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} ${d.value !== null ? d.value.toFixed(1) + ' ms' : 'Failed'} 0 ? ' style="color:#dc2626;font-weight:600;"' : ''}>${d.packet_loss?.toFixed(1) ?? 0}% ${getStatusText(d.value)} `).join('') // Generate chart SVG - data already expanded for realtime const chartData = report.isRealtime ? 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

' if (chartData.length >= 2) { const rawMin = Math.min(...chartData) const rawMax = Math.max(...chartData) // Ensure a minimum range of 10ms or 20% of the average to avoid flat lines const avgVal = chartData.reduce((a, b) => a + b, 0) / chartData.length const minRange = Math.max(10, avgVal * 0.2) const range = Math.max(rawMax - rawMin, minRange) // Center the data if range was expanded const midPoint = (rawMin + rawMax) / 2 const minVal = midPoint - range / 2 const maxVal = midPoint + range / 2 const width = 700 const height = 120 const padding = 40 const chartHeight = height - padding * 2 const chartWidth = width - padding * 2 const points = chartData.map((val, i) => { const x = padding + (i / (chartData.length - 1)) * chartWidth const y = padding + chartHeight - ((val - minVal) / range) * chartHeight return `${x},${y}` }).join(' ') const areaPoints = `${padding},${height - padding} ${points} ${width - padding},${height - padding}` chartSvg = ` ${Math.round(maxVal)}ms ${Math.round((minVal + maxVal) / 2)}ms ${Math.round(minVal)}ms ${chartData.length} samples ` } const html = ` Network Latency Report - ${report.targetLabel}
ProxMenux Network Latency Report
Review the report, then print or save as PDF
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}
3. Latency Graph
${chartSvg}
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 && report.realtimeResults.length > 0 ? `
5. Detailed Test Results
${realtimeTableRows}
# Time Latency Packet Loss Status
` : ''} ${!report.isRealtime && report.data.length > 0 ? `
5. Latency History (Last ${Math.min(20, report.data.length)} Records)
${historyTableRows}
# Time Latency Packet Loss Status
` : ''}
${(report.isRealtime && report.realtimeResults.length > 0) || (!report.isRealtime && report.data.length > 0) ? '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.' }

` // Use Blob URL for Safari-safe preview const blob = new Blob([html], { type: "text/html" }) const url = URL.createObjectURL(blob) window.open(url, "_blank") } 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 baseTime = Date.now() // Expand each ping result into 3 individual samples (min, avg, max) with slightly different timestamps // This ensures the graph shows all actual measured values, not just averages const samples: RealtimeResult[] = [] if (result.latency_min !== null) { samples.push({ ...result, latency_avg: result.latency_min, timestamp: baseTime - 200, // Slightly earlier }) } if (result.latency_avg !== null && result.latency_avg !== result.latency_min && result.latency_avg !== result.latency_max) { samples.push({ ...result, latency_avg: result.latency_avg, timestamp: baseTime, }) } if (result.latency_max !== null) { samples.push({ ...result, latency_avg: result.latency_max, timestamp: baseTime + 200, // Slightly later }) } // Fallback if no valid samples if (samples.length === 0 && result.latency_avg !== null) { samples.push({ ...result, timestamp: baseTime }) } setRealtimeResults(prev => [...prev, ...samples]) } } 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' }), })) // Data already expanded to individual ping values - just format for chart 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, packet_loss: r.packet_loss })) // Calculate realtime stats - all values are now individual ping measurements stored in latency_avg const validValues = realtimeResults.filter(r => r.latency_avg !== null).map(r => r.latency_avg!) const realtimeStats = validValues.length > 0 ? { current: validValues[validValues.length - 1], min: Math.min(...validValues), max: Math.max(...validValues), avg: validValues.reduce((acc, v) => acc + v, 0) / validValues.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 - Compact single row */}
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 ? ( `${Number(v).toFixed(1)}ms`} /> } /> ) : (

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

) ) : loading ? (
) : chartData.length > 0 ? ( `${Number(v).toFixed(1)}ms`} /> } /> {/* For longer timeframes (6h+), show max values to preserve spikes. For 1 hour view, show avg values since there's no downsampling */} ) : (

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.

)}
) }