From 8c51957bfab076e1f1540acab9ecbcdd31ba0887 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Sun, 8 Mar 2026 16:33:52 +0100 Subject: [PATCH] Update notification service --- AppImage/components/latency-detail-modal.tsx | 172 ++++++++++++------- AppImage/scripts/health_monitor.py | 8 +- 2 files changed, 113 insertions(+), 67 deletions(-) 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
- - - - - - - - - - - - - ${realtimeTableRows} - + + + + + + + + + + + ${realtimeTableRows} +
#TimeLatency (Avg)MinMaxPacket LossStatus
#TimeLatencyPacket LossStatus
` : ''} - +${!report.isRealtime && report.data.length > 0 ? ` +
-
${report.isRealtime ? '6' : '5'}. Methodology
+
5. Latency History (Last ${Math.min(24, report.data.length)} Records)
+ + + + + + + + + + + + ${historyTableRows} + +
#TimeLatencyPacket LossStatus
+
+` : ''} + + +
+
${(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 = {}