"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
<>
>
) : (
// Simple latency display for single data points
Latency:
{entry.value} ms
)}
{packetLoss !== undefined && packetLoss > 0 && (
)}
)
}
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 = `
`
}
const html = `
Network Latency Report - ${report.targetLabel}
ProxMenux Network Latency Report
Review the report, then print or save as PDF
1. Executive Summary
${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
| # |
Time |
Latency |
Packet Loss |
Status |
${realtimeTableRows}
` : ''}
${!report.isRealtime && report.data.length > 0 ? `
5. Latency History (Last ${Math.min(20, report.data.length)} Records)
| # |
Time |
Latency |
Packet Loss |
Status |
${historyTableRows}
` : ''}
${(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 (
)
}