2026-03-06 18:44:27 +01:00
"use client"
2026-03-06 20:02:49 +01:00
import { useState , useEffect , useCallback , useRef } from "react"
2026-03-06 18:44:27 +01:00
import { Dialog , DialogContent , DialogHeader , DialogTitle } from "./ui/dialog"
import { Select , SelectContent , SelectItem , SelectTrigger , SelectValue } from "./ui/select"
2026-03-06 19:32:10 +01:00
import { Button } from "./ui/button"
import { Badge } from "./ui/badge"
2026-03-06 20:02:49 +01:00
import { Activity , TrendingDown , TrendingUp , Minus , RefreshCw , Wifi , FileText , Square } from "lucide-react"
2026-03-06 19:32:10 +01:00
import { AreaChart , Area , XAxis , YAxis , CartesianGrid , Tooltip , ResponsiveContainer , LineChart , Line } from "recharts"
2026-03-06 18:44:27 +01:00
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 = [
2026-03-07 17:57:57 +01:00
{ 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 } ,
]
2026-03-06 18:44:27 +01:00
2026-03-06 20:02:49 +01:00
// Realtime test configuration
const REALTIME_TEST_DURATION = 120 // 2 minutes in seconds
const REALTIME_TEST_INTERVAL = 5 // 5 seconds between tests
2026-03-06 18:44:27 +01:00
interface LatencyHistoryPoint {
timestamp : number
value : number
min? : number
max? : number
packet_loss? : number
}
interface LatencyStats {
min : number
max : number
avg : number
current : number
}
2026-03-06 19:32:10 +01:00
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
}
2026-03-06 18:44:27 +01:00
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 (
< div className = "bg-gray-900/95 backdrop-blur-sm border border-gray-700 rounded-lg p-3 shadow-xl" >
< p className = "text-sm font-semibold text-white mb-2" > { label } < / p >
< div className = "space-y-1.5" >
< div className = "flex items-center gap-2" >
< div className = "w-2.5 h-2.5 rounded-full flex-shrink-0 bg-blue-500" / >
< span className = "text-xs text-gray-300 min-w-[60px]" > Latency : < / span >
< span className = "text-sm font-semibold text-white" > { entry . value } ms < / span >
< / div >
{ packetLoss !== undefined && packetLoss > 0 && (
< div className = "flex items-center gap-2" >
< div className = "w-2.5 h-2.5 rounded-full flex-shrink-0 bg-red-500" / >
< span className = "text-xs text-gray-300 min-w-[60px]" > Pkt Loss : < / span >
< span className = "text-sm font-semibold text-red-400" > { packetLoss } % < / span >
< / div >
) }
< / div >
< / div >
)
}
return null
}
const getStatusColor = ( latency : number ) = > {
if ( latency >= 200 ) return "#ef4444"
if ( latency >= 100 ) return "#f59e0b"
return "#22c55e"
}
2026-03-06 19:32:10 +01:00
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" }
2026-03-06 18:44:27 +01:00
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" }
}
2026-03-06 19:32:10 +01:00
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
2026-03-06 20:02:49 +01:00
testDuration? : number
2026-03-06 19:32:10 +01:00
}
const generateLatencyReport = ( report : ReportData ) = > {
const now = new Date ( ) . toLocaleString ( )
2026-03-06 20:02:49 +01:00
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
2026-03-06 19:32:10 +01:00
const statusText = report . isRealtime
2026-03-06 20:02:49 +01:00
? getStatusText ( realtimeStats ? . current ? ? null )
2026-03-06 19:32:10 +01:00
: getStatusText ( report . stats . current )
2026-03-06 20:02:49 +01:00
// Colors matching Lynis report
2026-03-06 19:32:10 +01:00
const statusColorMap : Record < string , string > = {
2026-03-06 20:02:49 +01:00
"Excellent" : "#16a34a" ,
"Good" : "#16a34a" ,
"Fair" : "#ca8a04" ,
"Poor" : "#dc2626" ,
"N/A" : "#64748b"
2026-03-06 19:32:10 +01:00
}
2026-03-06 20:02:49 +01:00
const statusColor = statusColorMap [ statusText ] || "#64748b"
2026-03-06 19:32:10 +01:00
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 ) = > `
2026-03-06 20:02:49 +01:00
< tr $ { r.packet_loss > 0 ? ' class="warn"' : '' } >
2026-03-06 19:32:10 +01:00
< td > $ { i + 1 } < / td >
< td > $ { new Date ( r . timestamp || Date . now ( ) ) . toLocaleTimeString ( ) } < / td >
2026-03-06 20:02:49 +01:00
< td style = "font-weight:600;color:${statusColorMap[getStatusText(r.latency_avg)] || '#64748b'}" > $ { r . latency_avg !== null ? r . latency_avg + ' ms' : 'Failed' } < / td >
2026-03-06 19:32:10 +01:00
< td > $ { r . latency_min !== null ? r . latency_min + ' ms' : '-' } < / td >
< td > $ { r . latency_max !== null ? r . latency_max + ' ms' : '-' } < / td >
2026-03-06 20:02:49 +01:00
< td $ { r.packet_loss > 0 ? ' style="color:#dc2626;font-weight:600;"' : '' } > $ { r . packet_loss } % < / td >
< td > < span class = "f-tag" style = "background:${statusColorMap[getStatusText(r.latency_avg)] || '#64748b'}15;color:${statusColorMap[getStatusText(r.latency_avg)] || '#64748b'}" > $ { getStatusText ( r . latency_avg ) } < / span > < / td >
2026-03-06 19:32:10 +01:00
< / tr >
` ).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
2026-03-06 20:34:59 +01:00
// Generate chart SVG
const chartData = report . isRealtime
? report . realtimeResults . map ( r = > r . latency_avg || 0 )
: report . data . map ( d = > d . value || 0 )
let chartSvg = '<p style="text-align:center;color:#64748b;padding:20px;">Not enough data points for chart</p>'
if ( chartData . length >= 2 ) {
2026-03-06 20:54:40 +01:00
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
2026-03-06 20:34:59 +01:00
const width = 700
const height = 120
2026-03-06 20:54:40 +01:00
const padding = 40
const chartHeight = height - padding * 2
const chartWidth = width - padding * 2
2026-03-06 20:34:59 +01:00
const points = chartData . map ( ( val , i ) = > {
2026-03-06 20:54:40 +01:00
const x = padding + ( i / ( chartData . length - 1 ) ) * chartWidth
const y = padding + chartHeight - ( ( val - minVal ) / range ) * chartHeight
2026-03-06 20:34:59 +01:00
return ` ${ x } , ${ y } `
} ) . join ( ' ' )
const areaPoints = ` ${ padding } , ${ height - padding } ${ points } ${ width - padding } , ${ height - padding } `
chartSvg = `
< svg width = "100%" viewBox = "0 0 ${width} ${height}" style = "display:block;" >
< defs >
< linearGradient id = "areaGrad" x1 = "0" y1 = "0" x2 = "0" y2 = "1" >
< stop offset = "0%" stop-color = "${statusColor}" stop-opacity = "0.3" / >
< stop offset = "100%" stop-color = "${statusColor}" stop-opacity = "0.05" / >
< / linearGradient >
< / defs >
< line x1 = "${padding}" y1 = "${padding}" x2 = "${padding}" y2 = "${height - padding}" stroke = "#e2e8f0" stroke-width = "1" / >
< line x1 = "${padding}" y1 = "${height - padding}" x2 = "${width - padding}" y2 = "${height - padding}" stroke = "#e2e8f0" stroke-width = "1" / >
< line x1 = "${padding}" y1 = "${height / 2}" x2 = "${width - padding}" y2 = "${height / 2}" stroke = "#e2e8f0" stroke-width = "1" stroke-dasharray = "4" / >
2026-03-06 20:54:40 +01:00
< text x = "${padding - 5}" y = "${padding + 4}" font-size = "9" fill = "#64748b" text-anchor = "end" > $ { Math . round ( maxVal ) } ms < / text >
< text x = "${padding - 5}" y = "${height / 2 + 3}" font-size = "9" fill = "#64748b" text-anchor = "end" > $ { Math . round ( ( minVal + maxVal ) / 2 ) } ms < / text >
< text x = "${padding - 5}" y = "${height - padding + 4}" font-size = "9" fill = "#64748b" text-anchor = "end" > $ { Math . round ( minVal ) } ms < / text >
2026-03-06 20:34:59 +01:00
< polygon points = "${areaPoints}" fill = "url(#areaGrad)" / >
< polyline points = "${points}" fill = "none" stroke = "${statusColor}" stroke-width = "2" / >
< text x = "${width / 2}" y = "${height - 5}" font-size = "9" fill = "#64748b" text-anchor = "middle" > $ { chartData . length } samples < / text >
< / svg >
`
}
2026-03-06 19:32:10 +01:00
const html = ` <!DOCTYPE html>
< html lang = "en" >
< head >
2026-03-06 20:02:49 +01:00
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1" >
< title > Network Latency Report - $ { report . targetLabel } < / title >
< style >
* { margin : 0 ; padding : 0 ; box - sizing : border - box ; }
body { font - family : - apple - system , BlinkMacSystemFont , 'Segoe UI' , Roboto , sans - serif ; color : # 1 a1a2e ; background : # fff ; font - size : 13px ; line - height : 1.5 ; }
2026-03-07 16:23:11 +01:00
@page { margin : 15mm 15 mm 20 mm 15 mm ; size : A4 ; }
2026-03-06 20:02:49 +01:00
@media print {
. no - print { display : none ! important ; }
. page - break { page - break - before : always ; }
* { - webkit - print - color - adjust : exact ! important ; print - color - adjust : exact ! important ; }
2026-03-07 17:21:44 +01:00
body { font - size : 11px ; }
2026-03-06 20:02:49 +01:00
. section { margin - bottom : 16px ; }
. rpt - header - left p , . rpt - header - right { color : # 374151 ; }
. rpt - header - right . rid { color : # 4 b5563 ; }
. exec - text p { color : # 374151 ; }
. score - bar - labels { color : # 4 b5563 ; }
. card - label { color : # 4 b5563 ; }
. card - sub { color : # 374151 ; }
. chk - tbl th { color : # 374151 ; }
. rpt - footer { color : # 4 b5563 ; }
}
@media screen {
2026-03-07 16:23:11 +01:00
body { max - width : 1000px ; margin : 0 auto ; padding : 24px 32 px ; padding - top : 64px ; }
2026-03-06 20:02:49 +01:00
}
/* Top bar for screen only */
. top - bar {
position : fixed ; top : 0 ; left : 0 ; right : 0 ; background : # 0 f172a ; color : # e2e8f0 ;
padding : 12px 24 px ; display : flex ; align - items : center ; justify - content : space - between ; z - index : 100 ;
font - size : 13px ;
}
. top - bar button {
2026-03-06 21:25:14 +01:00
background : # 06 b6d4 ; color : # fff ; border : none ; padding : 8px 20 px ; border - radius : 6px ;
font - size : 13px ; font - weight : 600 ; cursor : pointer ;
2026-03-06 20:02:49 +01:00
}
. top - bar button :hover { background : # 0891 b2 ; }
2026-03-06 21:25:14 +01:00
. top - bar . close - btn {
background : rgba ( 255 , 255 , 255 , 0.1 ) ; color : # fff ; border : 1px solid rgba ( 255 , 255 , 255 , 0.2 ) ;
padding : 6px 12 px ; border - radius : 6px ; display : flex ; align - items : center ; gap : 6px ;
cursor : pointer ; font - size : 13px ; font - weight : 500 ;
}
. top - bar . close - btn :hover { background : rgba ( 255 , 255 , 255 , 0.2 ) ; }
. top - bar . close - btn . close - text { display : none ; }
. hide - mobile { }
2026-03-06 20:02:49 +01:00
@media print { . top - bar { display : none ; } body { padding - top : 0 ; } }
2026-03-06 21:25:14 +01:00
@media screen and ( max - width : 600px ) {
2026-03-07 16:23:11 +01:00
. top - bar { padding : 10px 12 px ; }
2026-03-06 21:25:14 +01:00
. hide - mobile { display : none ! important ; }
2026-03-07 16:23:11 +01:00
. top - bar . close - btn { padding : 8px 16 px ; font - size : 14px ; }
2026-03-06 21:25:14 +01:00
. top - bar . close - btn . close - text { display : inline ; }
2026-03-07 16:23:11 +01:00
body { padding - top : 60px ; }
2026-03-06 21:25:14 +01:00
}
2026-03-06 20:02:49 +01:00
/* Header */
. rpt - header {
display : flex ; align - items : center ; justify - content : space - between ;
2026-03-07 16:23:11 +01:00
padding : 18px 0 ; border - bottom : 3px solid # 0 f172a ; margin - bottom : 22px ;
2026-03-06 20:02:49 +01:00
}
2026-03-07 16:23:11 +01:00
. rpt - header - left { display : flex ; align - items : center ; gap : 14px ; }
. rpt - header - left img { height : 44px ; width : auto ; }
. rpt - header - left h1 { font - size : 22px ; font - weight : 700 ; color : # 0 f172a ; }
. rpt - header - left p { font - size : 11px ; color : # 64748 b ; }
. rpt - header - right { text - align : right ; font - size : 11px ; color : # 64748 b ; line - height : 1.6 ; }
. rpt - header - right . rid { font - family : monospace ; font - size : 10px ; color : # 94 a3b8 ; }
2026-03-06 20:02:49 +01:00
/* Sections */
2026-03-07 16:23:11 +01:00
. section { margin - bottom : 22px ; }
2026-03-06 20:02:49 +01:00
. section - title {
2026-03-07 16:23:11 +01:00
font - size : 14px ; font - weight : 700 ; color : # 0 f172a ; text - transform : uppercase ;
letter - spacing : 0.05em ; padding - bottom : 5px ; border - bottom : 2px solid # e2e8f0 ; margin - bottom : 12px ;
2026-03-06 20:02:49 +01:00
}
/* Executive summary */
. exec - box {
2026-03-07 16:23:11 +01:00
display : flex ; align - items : center ; gap : 24px ; padding : 20px ;
background : # f8fafc ; border : 1px solid # e2e8f0 ; border - radius : 8px ; margin - bottom : 16px ;
2026-03-06 20:02:49 +01:00
}
. score - ring {
width : 96px ; height : 96px ; border - radius : 50 % ; display : flex ; flex - direction : column ;
align - items : center ; justify - content : center ; border : 4px solid ; flex - shrink : 0 ;
}
2026-03-07 16:23:11 +01:00
. score - num { font - size : 32px ; font - weight : 800 ; line - height : 1 ; }
. score - unit { font - size : 14px ; font - weight : 600 ; opacity : 0.8 ; }
2026-03-06 20:02:49 +01:00
. score - lbl { font - size : 9px ; font - weight : 700 ; letter - spacing : 0.1em ; text - transform : uppercase ; margin - top : 2px ; }
. exec - text { flex : 1 ; }
. exec - text h3 { font - size : 16px ; margin - bottom : 4px ; }
. exec - text p { font - size : 12px ; color : # 64748 b ; line - height : 1.5 ; }
2026-03-06 20:34:59 +01:00
/* Latency gauge */
. latency - gauge {
display : flex ; flex - direction : column ; align - items : center ; flex - shrink : 0 ; width : 160px ;
}
. gauge - value { display : flex ; align - items : baseline ; gap : 2px ; margin - top : - 10 px ; }
. gauge - num { font - size : 32px ; font - weight : 800 ; line - height : 1 ; }
. gauge - unit { font - size : 14px ; font - weight : 600 ; opacity : 0.8 ; }
. gauge - status { font - size : 10px ; font - weight : 700 ; letter - spacing : 0.1em ; text - transform : uppercase ; margin - top : 2px ; }
/* Latency range display */
. latency - range {
display : flex ; gap : 24px ; margin - top : 12px ; padding - top : 12px ; border - top : 1px solid # e2e8f0 ;
}
. range - item { display : flex ; flex - direction : column ; gap : 2px ; }
. range - label { font - size : 10px ; font - weight : 600 ; color : # 94 a3b8 ; text - transform : uppercase ; }
. range - value { font - size : 16px ; font - weight : 700 ; }
2026-03-06 20:02:49 +01:00
/* Grids */
. grid - 2 { display : grid ; grid - template - columns : 1fr 1 fr ; gap : 8px ; margin - bottom : 8px ; }
. grid - 3 { display : grid ; grid - template - columns : 1fr 1 fr 1 fr ; gap : 8px ; margin - bottom : 8px ; }
. grid - 4 { display : grid ; grid - template - columns : 1fr 1 fr 1 fr 1 fr ; gap : 8px ; margin - bottom : 8px ; }
. card { padding : 10px 12 px ; background : # f8fafc ; border : 1px solid # e2e8f0 ; border - radius : 6px ; }
. card - label { font - size : 10px ; font - weight : 600 ; color : # 94 a3b8 ; text - transform : uppercase ; letter - spacing : 0.05em ; margin - bottom : 2px ; }
. card - value { font - size : 13px ; font - weight : 600 ; color : # 0 f172a ; }
. card - c { text - align : center ; }
. card - c . card - value { font - size : 20px ; font - weight : 800 ; }
. card - c . card - label { margin - top : 3px ; margin - bottom : 0 ; }
. card - sub { font - size : 9px ; color : # 64748 b ; margin - top : 2px ; }
/* Tags */
. f - tag { font - size : 9px ; padding : 2px 6 px ; border - radius : 4px ; font - weight : 600 ; }
/* Tables */
. chk - tbl { width : 100 % ; border - collapse : collapse ; font - size : 11px ; margin - bottom : 14px ; }
. chk - tbl th { text - align : left ; padding : 6px 8 px ; font - size : 10px ; color : # 64748 b ; font - weight : 600 ; border - bottom : 1px solid # e2e8f0 ; background : # f1f5f9 ; }
. chk - tbl td { padding : 5px 8 px ; border - bottom : 1px solid # f1f5f9 ; color : # 1 e293b ; }
. chk - tbl tr . warn { background : # fef2f2 ; }
/* Thresholds */
. threshold - item {
display : flex ; align - items : center ; gap : 10px ; padding : 8px 12 px ;
background : # f8fafc ; border : 1px solid # e2e8f0 ; border - radius : 4px ; margin - bottom : 6px ;
}
. threshold - dot { width : 10px ; height : 10px ; border - radius : 50 % ; flex - shrink : 0 ; }
. threshold - item p { font - size : 11px ; color : # 374151 ; }
. threshold - item strong { color : # 0 f172a ; }
/* Info box */
. info - box {
background : # ecfeff ; border : 1px solid # a5f3fc ; border - left : 4px solid # 06 b6d4 ;
border - radius : 4px ; padding : 12px 14 px ; margin - top : 12px ;
}
. info - box h4 { font - size : 11px ; font - weight : 700 ; color : # 0891 b2 ; margin - bottom : 4px ; }
. info - box p { font - size : 11px ; color : # 0 e7490 ; }
/* Footer */
. rpt - footer {
margin - top : 32px ; padding - top : 12px ; border - top : 1px solid # e2e8f0 ;
display : flex ; justify - content : space - between ; font - size : 10px ; color : # 94 a3b8 ;
}
2026-03-06 20:34:59 +01:00
/* Print styles */
@media print {
* { - webkit - print - color - adjust : exact ! important ; print - color - adjust : exact ! important ; }
body { padding : 10mm ; font - size : 10pt ; }
. no - print { display : none ! important ; }
. container { max - width : 100 % ; padding : 0 ; }
/* Prevent page breaks inside elements */
. section { page - break - inside : avoid ; break - inside : avoid ; }
. exec - box { page - break - inside : avoid ; break - inside : avoid ; }
. card { page - break - inside : avoid ; break - inside : avoid ; }
. threshold - item { page - break - inside : avoid ; break - inside : avoid ; }
. info - box { page - break - inside : avoid ; break - inside : avoid ; }
. chk - tbl { page - break - inside : avoid ; break - inside : avoid ; }
. latency - gauge { page - break - inside : avoid ; break - inside : avoid ; }
. latency - range { page - break - inside : avoid ; break - inside : avoid ; }
/* Force page breaks before major sections if needed */
. section { page - break - before : auto ; }
/* Keep headers with their content */
. section - title { page - break - after : avoid ; break - after : avoid ; }
/* Ensure grids don't break awkwardly */
. grid - 2 , . grid - 3 , . grid - 4 { page - break - inside : avoid ; break - inside : avoid ; }
/* Table rows - try to keep together */
. chk - tbl tr { page - break - inside : avoid ; break - inside : avoid ; }
. chk - tbl thead { display : table - header - group ; }
/* Footer always at bottom */
. rpt - footer {
page - break - inside : avoid ; break - inside : avoid ;
margin - top : 20px ;
}
/* Reduce spacing for print */
. section { margin - bottom : 15px ; }
. exec - box { padding : 12px ; }
/* Ensure SVG charts print correctly */
svg { max - width : 100 % ; height : auto ; }
}
/* Mobile print adjustments */
@media print and ( max - width : 600px ) {
. exec - box { flex - direction : column ; gap : 15px ; }
. latency - gauge { width : 100 % ; }
. grid - 2 , . grid - 3 , . grid - 4 { grid - template - columns : 1fr 1 fr ; }
. latency - range { flex - wrap : wrap ; gap : 12px ; }
}
2026-03-06 20:02:49 +01:00
< / style >
2026-03-06 19:32:10 +01:00
< / head >
< body >
2026-03-06 20:02:49 +01:00
< script >
function pmxPrint ( ) {
try { window . print ( ) ; }
catch ( e ) {
var isMac = navigator . platform . toUpperCase ( ) . indexOf ( 'MAC' ) >= 0 ;
var el = document . getElementById ( 'pmx-print-hint' ) ;
if ( el ) el . textContent = isMac ? 'Use Cmd+P to save as PDF' : 'Use Ctrl+P to save as PDF' ;
}
}
< / script >
2026-03-06 21:25:14 +01:00
< div class = "top-bar no-print" >
< div style = "display:flex;align-items:center;gap:12px;" >
< button onclick = "window.close();window.history.back();" class = "close-btn" title = "Close" >
< svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" stroke-linecap = "round" stroke-linejoin = "round" > < line x1 = "18" y1 = "6" x2 = "6" y2 = "18" > < / line > < line x1 = "6" y1 = "6" x2 = "18" y2 = "18" > < / line > < / svg >
< span class = "close-text" > Close < / span >
< / button >
< strong class = "hide-mobile" > ProxMenux Network Latency Report < / strong >
< span id = "pmx-print-hint" class = "hide-mobile" style = "font-size:11px;opacity:0.7;" > Review the report , then print or save as PDF < / span >
< / div >
< div style = "display:flex;align-items:center;gap:8px;" >
< span class = "hide-mobile" style = "font-size:11px;opacity:0.5;" > ⌘ P / Ctrl + P < / span >
< button onclick = "pmxPrint()" > Print / Save as PDF < / button >
< / div >
2026-03-06 20:02:49 +01:00
< / div >
<!-- Header -->
< div class = "rpt-header" >
< div class = "rpt-header-left" >
< img src = "${logoUrl}" alt = "ProxMenux" onerror = "this.style.display='none'" / >
< div >
< h1 > Network Latency Report < / h1 >
< p > ProxMenux Monitor - Network Performance Analysis < / p >
2026-03-06 19:32:10 +01:00
< / div >
2026-03-06 20:02:49 +01:00
< / div >
< div class = "rpt-header-right" >
< div > < strong > Date : < / strong > $ { now } < / div >
< div > < strong > Target : < / strong > $ { report . targetLabel } < / div >
< div > < strong > Mode : < / strong > $ { report . isRealtime ? 'Real-time Test' : 'Historical Analysis' } < / div >
< div class = "rid" > ID : PMXL - $ { Date . now ( ) . toString ( 36 ) . toUpperCase ( ) } < / div >
< / div >
< / div >
<!-- 1. Executive Summary -->
< div class = "section" >
< div class = "section-title" > 1 . Executive Summary < / div >
< div class = "exec-box" >
2026-03-06 20:34:59 +01:00
< div class = "latency-gauge" >
2026-03-07 17:57:57 +01:00
< svg viewBox = "0 0 120 80" width = "160" height = "107" >
2026-03-06 20:34:59 +01:00
<!-- Gauge background arc -->
< path d = "M 10 70 A 50 50 0 0 1 110 70" fill = "none" stroke = "#e2e8f0" stroke-width = "8" stroke-linecap = "round" / >
<!-- Colored segments: Excellent (green), Good (green), Fair (yellow), Poor (red) -->
< path d = "M 10 70 A 50 50 0 0 1 35 28" fill = "none" stroke = "#16a34a" stroke-width = "8" stroke-linecap = "round" / >
< path d = "M 35 28 A 50 50 0 0 1 60 20" fill = "none" stroke = "#22c55e" stroke-width = "8" / >
< path d = "M 60 20 A 50 50 0 0 1 85 28" fill = "none" stroke = "#ca8a04" stroke-width = "8" / >
< path d = "M 85 28 A 50 50 0 0 1 110 70" fill = "none" stroke = "#dc2626" stroke-width = "8" stroke-linecap = "round" / >
<!-- Needle -->
< line x1 = "60" y1 = "70" x2 = "${60 + 40 * Math.cos(Math.PI - (Math.min(300, report.isRealtime ? (realtimeStats?.avg ?? 0) : parseFloat(String(report.stats.avg))) / 300) * Math.PI)}" y2 = "${70 - 40 * Math.sin(Math.PI - (Math.min(300, report.isRealtime ? (realtimeStats?.avg ?? 0) : parseFloat(String(report.stats.avg))) / 300) * Math.PI)}" stroke = "${statusColor}" stroke-width = "3" stroke-linecap = "round" / >
< circle cx = "60" cy = "70" r = "6" fill = "${statusColor}" / >
<!-- Labels -->
< text x = "8" y = "78" font-size = "7" fill = "#64748b" > 0 < / text >
< text x = "105" y = "78" font-size = "7" fill = "#64748b" > 300 + < / text >
< / svg >
< div class = "gauge-value" style = "color:${statusColor};" >
< span class = "gauge-num" > $ { report . isRealtime ? ( realtimeStats ? . avg ? . toFixed ( 0 ) ? ? 'N/A' ) : report . stats . avg } < / span >
< span class = "gauge-unit" > ms < / span >
< / div >
< div class = "gauge-status" style = "color:${statusColor};" > $ { statusText } < / div >
2026-03-06 20:02:49 +01:00
< / div >
< div class = "exec-text" >
< h3 > Network Latency Assessment $ { report . isRealtime ? ' (Real-time)' : '' } < / h3 >
< p >
$ { report . isRealtime
? ` Real-time latency test to <strong> ${ report . targetLabel } </strong> with <strong> ${ report . realtimeResults . length } samples</strong> collected over ${ report . testDuration ? Math . round ( report . testDuration / 60 ) + ' minute(s)' : 'the test period' } .
Average latency : < strong style = "color:${statusColor}" > $ { realtimeStats ? . avg ? . toFixed ( 1 ) ? ? 'N/A' } ms < / strong > .
$ { realtimeStats && realtimeStats . avgPacketLoss > 0 ? ` <span style="color:#dc2626">Average packet loss: ${ realtimeStats . avgPacketLoss . toFixed ( 1 ) } %.</span> ` : '<span style="color:#16a34a">No packet loss detected.</span>' } `
: ` Historical latency analysis to <strong>Gateway</strong> over <strong> ${ timeframeLabel . toLowerCase ( ) } </strong>.
< strong > $ { report . data . length } samples < / strong > analyzed .
Average latency : < strong style = "color:${statusColor}" > $ { report . stats . avg } ms < / strong > . `
}
< / p >
2026-03-06 20:34:59 +01:00
< div class = "latency-range" >
< div class = "range-item" >
< span class = "range-label" > Minimum < / span >
< span class = "range-value" style = "color:#16a34a;" > $ { report . isRealtime ? ( realtimeStats ? . min ? . toFixed ( 1 ) ? ? 'N/A' ) : report . stats . min } ms < / span >
< / div >
< div class = "range-item" >
< span class = "range-label" > Average < / span >
< span class = "range-value" style = "color:${statusColor};" > $ { report . isRealtime ? ( realtimeStats ? . avg ? . toFixed ( 1 ) ? ? 'N/A' ) : report . stats . avg } ms < / span >
2026-03-06 19:32:10 +01:00
< / div >
2026-03-06 20:34:59 +01:00
< div class = "range-item" >
< span class = "range-label" > Maximum < / span >
< span class = "range-value" style = "color:#dc2626;" > $ { report . isRealtime ? ( realtimeStats ? . max ? . toFixed ( 1 ) ? ? 'N/A' ) : report . stats . max } ms < / span >
2026-03-06 19:32:10 +01:00
< / div >
< / div >
< / div >
2026-03-06 20:02:49 +01:00
< / div >
< / div >
<!-- 2. Statistics -->
< div class = "section" >
< div class = "section-title" > 2 . Latency Statistics < / div >
< div class = "grid-4" >
< div class = "card card-c" >
< div class = "card-value" style = "color:${statusColor};" > $ { report . isRealtime ? ( realtimeStats ? . current ? . toFixed ( 1 ) ? ? 'N/A' ) : report . stats . current } < span style = "font-size:10px;color:#64748b;" > ms < / span > < / div >
< div class = "card-label" > Current < / div >
2026-03-06 19:32:10 +01:00
< / div >
2026-03-06 20:02:49 +01:00
< div class = "card card-c" >
< div class = "card-value" style = "color:#16a34a;" > $ { report . isRealtime ? ( realtimeStats ? . min ? . toFixed ( 1 ) ? ? 'N/A' ) : report . stats . min } < span style = "font-size:10px;color:#64748b;" > ms < / span > < / div >
< div class = "card-label" > Minimum < / div >
< / div >
< div class = "card card-c" >
< div class = "card-value" > $ { report . isRealtime ? ( realtimeStats ? . avg ? . toFixed ( 1 ) ? ? 'N/A' ) : report . stats . avg } < span style = "font-size:10px;color:#64748b;" > ms < / span > < / div >
< div class = "card-label" > Average < / div >
< / div >
< div class = "card card-c" >
< div class = "card-value" style = "color:#dc2626;" > $ { report . isRealtime ? ( realtimeStats ? . max ? . toFixed ( 1 ) ? ? 'N/A' ) : report . stats . max } < span style = "font-size:10px;color:#64748b;" > ms < / span > < / div >
< div class = "card-label" > Maximum < / div >
< / div >
< / div >
< div class = "grid-3" >
< div class = "card" >
< div class = "card-label" > Sample Count < / div >
< div class = "card-value" > $ { report . isRealtime ? report.realtimeResults.length : report.data.length } < / div >
< / div >
< div class = "card" >
< div class = "card-label" > Packet Loss ( Avg ) < / div >
< div class = "card-value" style = "color:${(report.isRealtime ? (realtimeStats?.avgPacketLoss ?? 0) : parseFloat(historyStats?.avgPacketLoss ?? '0')) > 0 ? '#dc2626' : '#16a34a'};" >
$ { report . isRealtime ? ( realtimeStats ? . avgPacketLoss ? . toFixed ( 1 ) ? ? '0' ) : ( historyStats ? . avgPacketLoss ? ? '0' ) } %
2026-03-06 19:32:10 +01:00
< / div >
< / div >
2026-03-06 20:02:49 +01:00
< div class = "card" >
< div class = "card-label" > Test Period < / div >
< div class = "card-value" style = "font-size:11px;" >
$ { report . isRealtime
? ( report . testDuration ? Math . round ( report . testDuration / 60 ) + ' min' : 'Real-time' )
: timeframeLabel }
2026-03-06 19:32:10 +01:00
< / div >
< / div >
2026-03-06 20:02:49 +01:00
< / div >
< / div >
$ { report . isRealtime && report . realtimeResults . length > 0 ? `
<!-- 3. Test Results -->
< div class = "section" >
< div class = "section-title" > 3 . Detailed Test Results < / div >
< table class = "chk-tbl" >
< thead >
< tr >
< th > # < / th >
< th > Time < / th >
< th > Latency ( Avg ) < / th >
< th > Min < / th >
< th > Max < / th >
< th > Packet Loss < / th >
< th > Status < / th >
< / tr >
< / thead >
< tbody >
$ { realtimeTableRows }
< / tbody >
< / table >
< / div >
` : ''}
2026-03-06 20:13:31 +01:00
<!-- Latency Chart -->
< div class = "section" >
2026-03-06 20:34:59 +01:00
< div class = "section-title" > $ { report . isRealtime ? '4' : '3' } . Latency Graph < / div >
2026-03-06 20:13:31 +01:00
< div style = "background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:16px;" >
2026-03-06 20:34:59 +01:00
$ { chartSvg }
2026-03-06 20:13:31 +01:00
< / div >
< / div >
<!-- Reference Thresholds -->
< div class = "section" >
2026-03-06 20:34:59 +01:00
< div class = "section-title" > $ { report . isRealtime ? '5' : '4' } . Performance Thresholds < / div >
2026-03-06 20:13:31 +01:00
< div class = "threshold-item" >
< div class = "threshold-dot" style = "background:#16a34a;" > < / div >
< p > < strong > Excellent ( & lt ; 50 ms ) : < / strong > Optimal for real - time applications , gaming , and video calls . < / p >
< / div >
< div class = "threshold-item" >
< div class = "threshold-dot" style = "background:#16a34a;" > < / div >
< p > < strong > Good ( 50 - 100 ms ) : < / strong > Acceptable for most applications with minimal impact . < / p >
< / div >
< div class = "threshold-item" >
< div class = "threshold-dot" style = "background:#ca8a04;" > < / div >
< p > < strong > Fair ( 100 - 200 ms ) : < / strong > Noticeable delay . May affect VoIP and interactive applications . < / p >
< / div >
< div class = "threshold-item" >
< div class = "threshold-dot" style = "background:#dc2626;" > < / div >
< p > < strong > Poor ( & gt ; 200 ms ) : < / strong > Significant latency . Investigation recommended . < / p >
< / div >
< / div >
2026-03-06 20:02:49 +01:00
<!-- Methodology -->
< div class = "section" >
2026-03-06 20:34:59 +01:00
< div class = "section-title" > $ { report . isRealtime ? '6' : '5' } . Methodology < / div >
2026-03-06 20:02:49 +01:00
< div class = "grid-2" >
< div class = "card" >
< div class = "card-label" > Test Method < / div >
< div class = "card-value" style = "font-size:12px;" > ICMP Echo Request ( Ping ) < / div >
< / div >
2026-03-06 20:13:31 +01:00
< div class = "card" >
< div class = "card-label" > Samples per Test < / div >
< div class = "card-value" style = "font-size:12px;" > 3 consecutive pings < / div >
< / div >
< div class = "card" >
< div class = "card-label" > Target < / div >
2026-03-06 20:34:59 +01:00
< div class = "card-value" style = "font-size:12px;" > $ { report . targetLabel } < / div >
2026-03-06 20:13:31 +01:00
< / div >
< div class = "card" >
< div class = "card-label" > Target IP < / div >
2026-03-06 20:34:59 +01:00
< div class = "card-value" style = "font-size:12px;" > $ { report . target === 'gateway' ? 'Default Gateway' : report . target === 'cloudflare' ? '1.1.1.1' : '8.8.8.8' } < / div >
2026-03-06 20:13:31 +01:00
< / div >
< / div >
< div class = "info-box" >
< h4 > Performance Assessment < / h4 >
2026-03-06 20:34:59 +01:00
< p > $ {
2026-03-06 20:13:31 +01:00
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.'
} < / p >
< / div >
< / div >
<!-- Footer -->
< div class = "rpt-footer" >
< div >
2026-03-06 20:34:59 +01:00
< img src = "${logoUrl}" alt = "ProxMenux" style = "height:20px;vertical-align:middle;margin-right:8px;" onerror = "this.style.display='none'" / >
2026-03-06 20:13:31 +01:00
ProxMenux Monitor - Network Performance Report
< / div >
2026-03-06 20:34:59 +01:00
< div > Generated : $ { now } | Report ID : PMXL - $ { Date . now ( ) . toString ( 36 ) . toUpperCase ( ) } < / div >
2026-03-06 20:13:31 +01:00
< / div >
2026-03-06 19:32:10 +01:00
< / body >
< / html > `
const printWindow = window . open ( '' , '_blank' )
if ( printWindow ) {
printWindow . document . write ( html )
printWindow . document . close ( )
}
}
2026-03-06 18:44:27 +01:00
export function LatencyDetailModal ( { open , onOpenChange , currentLatency } : LatencyDetailModalProps ) {
const [ timeframe , setTimeframe ] = useState ( "hour" )
const [ target , setTarget ] = useState ( "gateway" )
const [ data , setData ] = useState < LatencyHistoryPoint [ ] > ( [ ] )
const [ stats , setStats ] = useState < LatencyStats > ( { min : 0 , max : 0 , avg : 0 , current : 0 } )
const [ loading , setLoading ] = useState ( true )
2026-03-06 19:32:10 +01:00
const [ realtimeResults , setRealtimeResults ] = useState < RealtimeResult [ ] > ( [ ] )
const [ realtimeTesting , setRealtimeTesting ] = useState ( false )
2026-03-06 20:02:49 +01:00
const [ testProgress , setTestProgress ] = useState ( 0 ) // 0-100 percentage
const [ testStartTime , setTestStartTime ] = useState < number | null > ( null )
const testIntervalRef = useRef < NodeJS.Timeout | null > ( null )
2026-03-06 18:44:27 +01:00
const isMobile = useIsMobile ( )
2026-03-06 19:32:10 +01:00
const isRealtime = TARGET_OPTIONS . find ( t = > t . value === target ) ? . realtime ? ? false
2026-03-06 20:02:49 +01:00
// Cleanup on unmount or close
useEffect ( ( ) = > {
if ( ! open ) {
stopRealtimeTest ( )
}
return ( ) = > {
stopRealtimeTest ( )
}
} , [ open ] )
2026-03-06 19:32:10 +01:00
// Fetch history for gateway
2026-03-06 18:44:27 +01:00
useEffect ( ( ) = > {
2026-03-06 19:32:10 +01:00
if ( open && target === "gateway" ) {
2026-03-06 18:44:27 +01:00
fetchHistory ( )
}
} , [ open , timeframe , target ] )
2026-03-06 20:02:49 +01:00
// Auto-start test when switching to realtime target
2026-03-06 19:32:10 +01:00
useEffect ( ( ) = > {
if ( open && isRealtime ) {
2026-03-06 20:02:49 +01:00
// Clear previous results and start new test
2026-03-06 19:32:10 +01:00
setRealtimeResults ( [ ] )
2026-03-06 20:02:49 +01:00
startRealtimeTest ( )
} else {
stopRealtimeTest ( )
2026-03-06 19:32:10 +01:00
}
} , [ open , target ] )
2026-03-06 18:44:27 +01:00
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 ) {
2026-03-06 19:32:10 +01:00
// Silently fail
2026-03-06 18:44:27 +01:00
} finally {
setLoading ( false )
}
}
2026-03-06 20:02:49 +01:00
const runSingleTest = useCallback ( async ( ) = > {
2026-03-06 19:32:10 +01:00
try {
const result = await fetchApi < RealtimeResult > ( ` /api/network/latency/current?target= ${ target } ` )
if ( result ) {
2026-03-06 20:02:49 +01:00
const resultWithTimestamp = { . . . result , timestamp : Date.now ( ) }
setRealtimeResults ( prev = > [ . . . prev , resultWithTimestamp ] )
2026-03-06 19:32:10 +01:00
}
} catch ( err ) {
// Silently fail
}
2026-03-06 20:02:49 +01:00
} , [ target ] )
2026-03-06 18:44:27 +01:00
2026-03-06 20:02:49 +01:00
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 ] )
2026-03-06 19:32:10 +01:00
2026-03-06 20:02:49 +01:00
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' } ) ,
2026-03-06 18:44:27 +01:00
} ) )
2026-03-06 20:02:49 +01:00
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 ,
2026-03-06 19:32:10 +01:00
packet_loss : r.packet_loss ,
} ) )
2026-03-06 20:02:49 +01:00
// 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
2026-03-06 19:32:10 +01:00
2026-03-06 20:02:49 +01:00
const displayStats = isRealtime ? {
current : realtimeStats?.current ? ? 0 ,
min : realtimeStats?.min ? ? 0 ,
max : realtimeStats?.max ? ? 0 ,
avg : Math.round ( ( realtimeStats ? . avg ? ? 0 ) * 10 ) / 10 ,
} : stats
2026-03-06 18:44:27 +01:00
2026-03-06 20:02:49 +01:00
const statusInfo = getStatusInfo ( displayStats . current )
2026-03-06 18:44:27 +01:00
2026-03-06 20:13:31 +01:00
// 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
2026-03-06 19:32:10 +01:00
2026-03-06 18:44:27 +01:00
return (
< Dialog open = { open } onOpenChange = { onOpenChange } >
2026-03-06 20:02:49 +01:00
< DialogContent className = "max-w-4xl max-h-[90vh] overflow-y-auto bg-background border-border" >
2026-03-06 18:44:27 +01:00
< DialogHeader >
2026-03-06 21:25:14 +01:00
< DialogTitle className = "flex items-center gap-2 text-foreground" >
< Wifi className = "h-5 w-5 text-blue-500" / >
Network Latency
< / DialogTitle >
< / DialogHeader >
2026-03-07 17:57:57 +01:00
< div className = "flex items-center gap-2 mt-1 flex-nowrap" >
2026-03-07 12:02:20 +01:00
< Select value = { target } onValueChange = { setTarget } >
2026-03-07 17:57:57 +01:00
< SelectTrigger className = "w-[140px] sm:w-[180px] h-8 text-xs shrink-0" >
< span className = "truncate" >
{ TARGET_OPTIONS . find ( t = > t . value === target ) ? . shortLabel || target }
< / span >
2026-03-07 12:02:20 +01:00
< / SelectTrigger >
< SelectContent >
{ TARGET_OPTIONS . map ( opt = > (
< SelectItem key = { opt . value } value = { opt . value } className = "text-xs" >
{ opt . label }
< / SelectItem >
) ) }
< / SelectContent >
< / Select >
{ ! isRealtime && (
< Select value = { timeframe } onValueChange = { setTimeframe } >
2026-03-07 16:23:11 +01:00
< SelectTrigger className = "w-[100px] h-8 text-xs shrink-0" >
2026-03-06 21:25:14 +01:00
< SelectValue / >
< / SelectTrigger >
< SelectContent >
2026-03-07 12:02:20 +01:00
{ TIMEFRAME_OPTIONS . map ( opt = > (
2026-03-06 21:25:14 +01:00
< SelectItem key = { opt . value } value = { opt . value } className = "text-xs" >
{ opt . label }
< / SelectItem >
) ) }
< / SelectContent >
< / Select >
2026-03-07 12:02:20 +01:00
) }
{ isRealtime && (
realtimeTesting ? (
< Button
variant = "outline"
size = "sm"
onClick = { stopRealtimeTest }
2026-03-07 16:23:11 +01:00
className = "gap-1.5 text-red-500 border-red-500/30 hover:bg-red-500/10 shrink-0 h-8 px-3"
2026-03-07 12:02:20 +01:00
>
< Square className = "h-3 w-3 fill-current" / >
2026-03-07 16:23:11 +01:00
Stop
2026-03-07 12:02:20 +01:00
< / Button >
) : (
< Button
variant = "outline"
size = "sm"
onClick = { restartRealtimeTest }
2026-03-07 16:23:11 +01:00
className = "gap-1.5 shrink-0 h-8 px-3"
2026-03-07 12:02:20 +01:00
>
< RefreshCw className = "h-3 w-3" / >
2026-03-07 16:23:11 +01:00
Test Again
2026-03-07 12:02:20 +01:00
< / Button >
)
) }
2026-03-06 21:25:14 +01:00
< Button
variant = "outline"
size = "sm"
onClick = { ( ) = > generateLatencyReport ( {
target ,
targetLabel : TARGET_OPTIONS.find ( t = > t . value === target ) ? . label || target ,
isRealtime ,
stats ,
realtimeResults ,
data ,
timeframe ,
testDuration : isRealtime ? testDuration : undefined ,
} ) }
disabled = { isRealtime ? realtimeResults . length === 0 : data.length === 0 }
2026-03-07 16:23:11 +01:00
className = "gap-1.5 shrink-0 h-8 px-3"
2026-03-06 21:25:14 +01:00
>
2026-03-07 16:23:11 +01:00
< FileText className = "h-3.5 w-3.5" / >
Report
2026-03-06 21:25:14 +01:00
< / Button >
< / div >
2026-03-06 18:44:27 +01:00
2026-03-06 20:02:49 +01:00
{ /* Progress bar for realtime test */ }
{ isRealtime && realtimeTesting && (
< div className = "mb-4" >
< div className = "flex items-center justify-between text-xs text-muted-foreground mb-1" >
< span > Testing . . . { Math . round ( testProgress ) } % < / span >
< span > { Math . round ( ( REALTIME_TEST_DURATION * ( 1 - testProgress / 100 ) ) ) } s remaining < / span >
< / div >
< div className = "h-1.5 bg-muted rounded-full overflow-hidden" >
< div
className = "h-full bg-blue-500 transition-all duration-500 ease-out"
style = { { width : ` ${ testProgress } % ` } }
/ >
< / div >
2026-03-06 19:32:10 +01:00
< / div >
) }
2026-03-07 12:02:20 +01:00
{ /* Stats Cards - Compact single row */ }
< div className = "flex items-center justify-between gap-1 mb-2 py-2 px-1 bg-muted/20 rounded-lg" >
< div className = "flex items-center gap-1 min-w-0" >
< span className = "text-[10px] text-muted-foreground" > Current < / span >
< span className = "text-base font-bold" style = { { color : getStatusColor ( displayStats . current || 0 ) } } >
{ displayStats . current || '-' }
< / span >
< span className = "text-[10px] text-muted-foreground" > ms < / span >
2026-03-06 18:44:27 +01:00
< / div >
2026-03-07 12:02:20 +01:00
< div className = "flex items-center gap-1 min-w-0" >
< TrendingDown className = "h-3 w-3 text-green-500 shrink-0" / >
< span className = "text-[10px] text-muted-foreground" > Min < / span >
< span className = "text-base font-bold text-green-500" > { displayStats . min || '-' } < / span >
< span className = "text-[10px] text-muted-foreground" > ms < / span >
2026-03-06 20:02:49 +01:00
< / div >
2026-03-07 12:02:20 +01:00
< div className = "flex items-center gap-1 min-w-0" >
< Minus className = "h-3 w-3 shrink-0" / >
< span className = "text-[10px] text-muted-foreground" > Avg < / span >
< span className = "text-base font-bold" > { displayStats . avg || '-' } < / span >
< span className = "text-[10px] text-muted-foreground" > ms < / span >
2026-03-06 20:02:49 +01:00
< / div >
2026-03-07 12:02:20 +01:00
< div className = "flex items-center gap-1 min-w-0" >
< TrendingUp className = "h-3 w-3 text-red-500 shrink-0" / >
< span className = "text-[10px] text-muted-foreground" > Max < / span >
< span className = "text-base font-bold text-red-500" > { displayStats . max || '-' } < / span >
< span className = "text-[10px] text-muted-foreground" > ms < / span >
2026-03-06 20:02:49 +01:00
< / div >
< / div >
{ /* Status Badge */ }
< div className = "flex items-center justify-between mb-4" >
< Badge variant = "outline" className = { statusInfo . color } >
{ statusInfo . status }
< / Badge >
{ isRealtime && (
< span className = "text-xs text-muted-foreground" >
{ realtimeResults . length } sample { realtimeResults . length !== 1 ? 's' : '' } collected
{ realtimeStats ? . packetLoss ? ` | ${ realtimeStats . packetLoss } % packet loss ` : '' }
< / span >
2026-03-06 19:32:10 +01:00
) }
2026-03-06 18:44:27 +01:00
< / div >
{ /* Chart */ }
2026-03-06 20:02:49 +01:00
< div className = "h-[250px] sm:h-[300px] w-full" >
2026-03-06 19:32:10 +01:00
{ isRealtime ? (
2026-03-06 20:02:49 +01:00
realtimeChartData . length > 0 ? (
2026-03-06 19:32:10 +01:00
< ResponsiveContainer width = "100%" height = "100%" >
2026-03-06 20:02:49 +01:00
< LineChart data = { realtimeChartData } >
< CartesianGrid strokeDasharray = "3 3" stroke = "#374151" opacity = { 0.3 } / >
< XAxis
dataKey = "time"
stroke = "#6b7280"
fontSize = { 10 }
tickLine = { false }
2026-03-06 19:32:10 +01:00
interval = "preserveStartEnd"
/ >
2026-03-06 20:02:49 +01:00
< YAxis
stroke = "#6b7280"
fontSize = { 10 }
tickLine = { false }
domain = { [ 'dataMin - 5' , 'dataMax + 10' ] }
2026-03-06 19:32:10 +01:00
tickFormatter = { ( v ) = > ` ${ v } ms ` }
/ >
< Tooltip content = { < CustomTooltip / > } / >
< Line
type = "monotone"
dataKey = "value"
2026-03-06 20:02:49 +01:00
stroke = "#3b82f6"
2026-03-06 19:32:10 +01:00
strokeWidth = { 2 }
2026-03-06 20:02:49 +01:00
dot = { { fill : '#3b82f6' , strokeWidth : 0 , r : 3 } }
activeDot = { { r : 5 , fill : '#3b82f6' } }
2026-03-06 19:32:10 +01:00
/ >
< / LineChart >
< / ResponsiveContainer >
) : (
2026-03-06 20:02:49 +01:00
< div className = "h-full flex flex-col items-center justify-center text-muted-foreground" >
< Activity className = "h-12 w-12 mb-3 opacity-30" / >
< p className = "text-sm" >
{ realtimeTesting ? 'Collecting data...' : 'No data yet. Click "Test Again" to start.' }
< / p >
< / div >
2026-03-06 19:32:10 +01:00
)
2026-03-06 20:02:49 +01:00
) : loading ? (
< div className = "h-full flex items-center justify-center" >
< RefreshCw className = "h-8 w-8 animate-spin text-muted-foreground" / >
< / div >
) : chartData . length > 0 ? (
< ResponsiveContainer width = "100%" height = "100%" >
< AreaChart data = { chartData } >
< defs >
< linearGradient id = "latencyGradient" x1 = "0" y1 = "0" x2 = "0" y2 = "1" >
< stop offset = "5%" stopColor = "#3b82f6" stopOpacity = { 0.3 } / >
< stop offset = "95%" stopColor = "#3b82f6" stopOpacity = { 0 } / >
< / linearGradient >
< / defs >
< CartesianGrid strokeDasharray = "3 3" stroke = "#374151" opacity = { 0.3 } / >
< XAxis
dataKey = "time"
stroke = "#6b7280"
fontSize = { 10 }
tickLine = { false }
interval = "preserveStartEnd"
/ >
< YAxis
stroke = "#6b7280"
fontSize = { 10 }
tickLine = { false }
domain = { [ 'dataMin - 5' , 'dataMax + 10' ] }
tickFormatter = { ( v ) = > ` ${ v } ms ` }
/ >
< Tooltip content = { < CustomTooltip / > } / >
< Area
type = "monotone"
dataKey = "value"
stroke = "#3b82f6"
strokeWidth = { 2 }
fill = "url(#latencyGradient)"
/ >
< / AreaChart >
< / ResponsiveContainer >
) : (
< div className = "h-full flex flex-col items-center justify-center text-muted-foreground" >
< Activity className = "h-12 w-12 mb-3 opacity-30" / >
< p className = "text-sm" > No latency data available for this period < / p >
< p className = "text-xs mt-1" > Data is collected every 60 seconds < / p >
< / div >
2026-03-06 18:44:27 +01:00
) }
< / div >
2026-03-06 19:32:10 +01:00
2026-03-06 20:02:49 +01:00
{ /* Info for realtime mode */ }
{ isRealtime && (
< div className = "mt-4 p-3 bg-blue-500/10 border border-blue-500/20 rounded-lg" >
< p className = "text-xs text-blue-400" >
< strong > Real - time Mode : < / strong > Tests run for 2 minutes with readings every 5 seconds .
Click "Test Again" to add more samples . All data is included in the report .
< / p >
2026-03-06 19:32:10 +01:00
< / div >
) }
2026-03-06 18:44:27 +01:00
< / DialogContent >
< / Dialog >
)
}