diff --git a/AppImage/components/latency-detail-modal.tsx b/AppImage/components/latency-detail-modal.tsx
index e55c5111..176ac55a 100644
--- a/AppImage/components/latency-detail-modal.tsx
+++ b/AppImage/components/latency-detail-modal.tsx
@@ -124,12 +124,13 @@ 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,
+ // 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
@@ -149,14 +150,12 @@ const generateLatencyReport = (report: ReportData) => {
const timeframeLabel = TIMEFRAME_OPTIONS.find(t => t.value === report.timeframe)?.label || report.timeframe
- // Build test results table for realtime mode
+ // 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()} |
- ${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' : '-'} |
+ ${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)} |
@@ -170,17 +169,20 @@ const generateLatencyReport = (report: ReportData) => {
endTime: new Date(report.data[report.data.length - 1].timestamp * 1000).toLocaleString(),
} : null
- // Generate chart SVG - expand realtime to all 3 values (min, avg, max) per sample
+ // Build history table rows for gateway mode (last 24 records)
+ const historyTableRows = report.data.slice(-24).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.flatMap(r => {
- const points: number[] = []
- if (r.latency_min !== null) points.push(r.latency_min)
- if (r.latency_avg !== null && r.latency_avg !== r.latency_min && r.latency_avg !== r.latency_max) {
- points.push(r.latency_avg)
- }
- if (r.latency_max !== null) points.push(r.latency_max)
- return points.length > 0 ? points : [r.latency_avg ?? 0]
- })
+ ? report.realtimeResults.filter(r => r.latency_avg !== null).map(r => r.latency_avg!)
: report.data.map(d => d.value || 0)
let chartSvg = 'Not enough data points for chart
'
@@ -588,32 +590,51 @@ const generateLatencyReport = (report: ReportData) => {
-${report.isRealtime && report.realtimeResults.length > 0 ? `
-
-
+ ${report.isRealtime && report.realtimeResults.length > 0 ? `
+
+
5. Detailed Test Results
-
-
- | # |
- Time |
- Latency (Avg) |
- Min |
- Max |
- Packet Loss |
- Status |
-
-
-
- ${realtimeTableRows}
-
+
+
+ | # |
+ Time |
+ Latency |
+ Packet Loss |
+ Status |
+
+
+
+ ${realtimeTableRows}
+
` : ''}
-
+${!report.isRealtime && report.data.length > 0 ? `
+
-
${report.isRealtime ? '6' : '5'}. Methodology
+
5. Latency History (Last ${Math.min(24, 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
@@ -723,8 +744,39 @@ export function LatencyDetailModal({ open, onOpenChange, currentLatency }: Laten
try {
const result = await fetchApi
(`/api/network/latency/current?target=${target}`)
if (result) {
- const resultWithTimestamp = { ...result, timestamp: Date.now() }
- setRealtimeResults(prev => [...prev, resultWithTimestamp])
+ 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
@@ -779,28 +831,22 @@ export function LatencyDetailModal({ open, onOpenChange, currentLatency }: Laten
time: new Date(point.timestamp * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
}))
- // Expand each sample to 3 data points (min, avg, max) for accurate representation
- const realtimeChartData = realtimeResults.flatMap((r, i) => {
- const time = new Date(r.timestamp || Date.now()).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })
- const points = []
- if (r.latency_min !== null) points.push({ time, value: r.latency_min, packet_loss: r.packet_loss })
- if (r.latency_avg !== null && r.latency_avg !== r.latency_min && r.latency_avg !== r.latency_max) {
- points.push({ time, value: r.latency_avg, packet_loss: r.packet_loss })
- }
- if (r.latency_max !== null) points.push({ time, value: r.latency_max, packet_loss: r.packet_loss })
- // If no valid points, add avg as fallback
- if (points.length === 0 && r.latency_avg !== null) {
- points.push({ time, value: r.latency_avg, packet_loss: r.packet_loss })
- }
- return points
- })
+ // 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
- 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,
+ // 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
diff --git a/AppImage/scripts/health_monitor.py b/AppImage/scripts/health_monitor.py
index 2ca135bd..84167191 100644
--- a/AppImage/scripts/health_monitor.py
+++ b/AppImage/scripts/health_monitor.py
@@ -1961,20 +1961,20 @@ class HealthMonitor:
if interface.startswith('vmbr') or interface.startswith(('eth', 'ens', 'enp', 'eno')):
health_persistence.resolve_error(interface, 'Interface recovered')
- # Check connectivity (latency)
+ # Check connectivity (latency) - reads from gateway monitor database
latency_status = self._check_network_latency()
+ connectivity_check = {'status': 'OK', 'detail': 'Not tested'}
if latency_status:
latency_ms = latency_status.get('latency_ms', 'N/A')
latency_sev = latency_status.get('status', 'OK')
interface_details['connectivity'] = latency_status
+ detail_text = f'Latency {latency_ms}ms to gateway' if isinstance(latency_ms, (int, float)) else latency_status.get('reason', 'Unknown')
connectivity_check = {
'status': latency_sev if latency_sev not in ['UNKNOWN'] else 'OK',
- 'detail': f'Latency {latency_ms}ms to gateway' if isinstance(latency_ms, (int, float)) else latency_status.get('reason', 'Unknown'),
+ 'detail': detail_text,
}
if latency_sev not in ['OK', 'INFO', 'UNKNOWN']:
issues.append(latency_status.get('reason', 'Network latency issue'))
- else:
- connectivity_check = {'status': 'OK', 'detail': 'Not tested'}
# Build checks dict
checks = {}