Initial version
This commit is contained in:
		| @@ -0,0 +1,25 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Globalization; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
| using System.Windows; | ||||
| using System.Windows.Data; | ||||
|  | ||||
| namespace EonaCat.ConnectionMonitor.Converters | ||||
| { | ||||
|     public class StringToVisibilityConverter : IValueConverter | ||||
|     { | ||||
|         public object Convert(object value, Type targetType, object parameter, CultureInfo culture) | ||||
|         { | ||||
|             return string.IsNullOrWhiteSpace(value as string) ? Visibility.Collapsed : Visibility.Visible; | ||||
|         } | ||||
|  | ||||
|         public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) | ||||
|         { | ||||
|             throw new NotImplementedException(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -9,6 +9,11 @@ | ||||
|     <ApplicationIcon>EonaCat.ico</ApplicationIcon> | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="EonaCat.Json" Version="1.1.9" /> | ||||
|     <PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3485.44" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <Resource Include="EonaCat.ico" /> | ||||
|   </ItemGroup> | ||||
|   | ||||
							
								
								
									
										918
									
								
								EonaCat.ConnectionMonitor/Helpers/LeafLetHelper.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										918
									
								
								EonaCat.ConnectionMonitor/Helpers/LeafLetHelper.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,918 @@ | ||||
| using EonaCat.ConnectionMonitor.Models; | ||||
| using EonaCat.Json; | ||||
| using EonaCat.Json.Linq; | ||||
| using Microsoft.Web.WebView2.Wpf; | ||||
| using System.Collections.ObjectModel; | ||||
| using System.Diagnostics; | ||||
| using System.Net.Http; | ||||
|  | ||||
| namespace EonaCat.ConnectionMonitor.Helpers | ||||
| { | ||||
|     internal class LeafLetHelper | ||||
|     { | ||||
|         private readonly Dictionary<string, (double Lat, double Lon)> _countryCentroidCache = new(); | ||||
|         private readonly Dictionary<string, string> _countryFlagCache = new(); | ||||
|         private readonly HttpClient _mapHttpClient = new HttpClient(); | ||||
|         private WebView2 _mapWebView; | ||||
|         private (double Lat, double Lon)? _localCoord; | ||||
|  | ||||
|         private const string _mapHtml = @"<!doctype html> | ||||
| <html lang='en'> | ||||
| <head> | ||||
| <meta charset='utf-8'> | ||||
| <meta http-equiv='Content-Security-Policy' content=""default-src 'self' https: data: 'unsafe-inline' 'unsafe-eval'""> | ||||
| <meta name='viewport' content='width=device-width, initial-scale=1.0'> | ||||
| <link rel='stylesheet' href='https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'/> | ||||
| <link rel='stylesheet' href='https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css'/> | ||||
| <link rel='stylesheet' href='https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css'/> | ||||
|  | ||||
| <style> | ||||
| html, body { height:100%; margin:0; padding:0; font-family: 'Segoe UI', Arial, sans-serif;} | ||||
| #tabBar { display:flex; background:#222; color:#fff; } | ||||
| .tab { padding:8px 12px; cursor:pointer; flex:1; text-align:center; } | ||||
| .tab.active { background:#444; font-weight:bold; } | ||||
| #liveMapContainer, #historicalMapContainer { height:calc(100% - 36px); width:100%; position:relative; } | ||||
| .leaflet-popup-content-wrapper { background: #1f1f1f !important; color: #f0f0f0 !important; border-radius: 10px !important; padding: 10px 14px !important; font-size: 13px; } | ||||
| .pulse-marker { width:20px;height:20px;border-radius:50%;background:#ff3333;box-shadow:0 0 14px #ff3333;animation:pulse 1.5s infinite ease-in-out; } | ||||
| @keyframes pulse {0%{transform:scale(0.8);opacity:0.7;}50%{transform:scale(1.4);opacity:0.3;}100%{transform:scale(0.8);opacity:0.7;}} | ||||
| #statsPanel { position:absolute; top:10px; right:10px; background:rgba(25,25,25,0.9); color:#fff; padding:10px 12px; border-radius:6px; font-size:13px; z-index:1000; max-width:260px; display:flex; flex-direction:column; gap:8px; box-sizing:border-box; transition: all 0.3s ease; pointer-events:auto; } | ||||
| #statsPanel button, #statsPanel input[type='range'], #statsPanel select { width:100%; box-sizing:border-box; } | ||||
| #statsPanel.collapsed #statsContent { display:none; } | ||||
| #statsPanel.collapsed #statsHeader::after { content:' ▶'; } | ||||
| #statsPanel #statsHeader::after { content:' ▼'; } | ||||
| #topConnections { max-height:120px; overflow:auto; margin-top:6px; font-size:12px; color:#ccc; } | ||||
| #histControls { position:absolute; bottom:10px; left:10px; background:rgba(25,25,25,0.9); color:#fff; padding:6px 10px; border-radius:6px; font-size:12px; z-index:1000; display:flex; align-items:center; gap:6px; } | ||||
| #histControls input[type='range'] { width:120px; } | ||||
|  | ||||
| .leaflet-control-attribution { | ||||
|     display: none !important; | ||||
| } | ||||
|  | ||||
| .leaflet-control-logo,  | ||||
| .leaflet-control .leaflet-control-attribution img { | ||||
|     display: none !important; | ||||
| } | ||||
|  | ||||
| #histStatsPanel { | ||||
|     position:absolute; | ||||
|     top:10px; | ||||
|     right:10px; | ||||
|     background:rgba(25,25,25,0.9); | ||||
|     color:#fff; | ||||
|     padding:10px 12px; | ||||
|     border-radius:6px; | ||||
|     font-size:13px; | ||||
|     z-index:1000; | ||||
|     max-width:260px; | ||||
|     display:flex; | ||||
|     flex-direction:column; | ||||
|     gap:8px; | ||||
|     box-sizing:border-box; | ||||
|     transition: all 0.3s ease; | ||||
|     pointer-events:auto; | ||||
| } | ||||
|  | ||||
| #histStatsPanel.collapsed #histStatsContent { display:none; } | ||||
| #histStatsPanel.collapsed #histStatsHeader::after { content:' ▶'; } | ||||
| #histStatsPanel #histStatsHeader::after { content:' ▼'; } | ||||
|  | ||||
| </style> | ||||
| </head> | ||||
| <body> | ||||
|  | ||||
| <div id=""tabBar""> | ||||
|     <div class=""tab active"" id=""liveTabBtn"">Live Map</div> | ||||
|     <div class=""tab"" id=""histTabBtn"">Historical Map</div> | ||||
| </div> | ||||
|  | ||||
| <div id=""liveMapContainer""> | ||||
|     <div id=""map"" style=""height:100%; width:100%;""></div> | ||||
|     <div id=""statsPanel""> | ||||
|         <div id=""statsHeader"" style=""font-weight:bold; cursor:pointer; padding:4px; background:#333; border-radius:4px;"">Stats</div> | ||||
|         <div id=""statsContent"" style=""margin-top:6px;""> | ||||
|             <div id=""statsText"">Connections: 0 | Top Country: N/A</div> | ||||
|             <button id=""toggleTrailsBtn"" style=""margin-top:6px; padding:6px 8px; border:none; border-radius:4px; background:#444; color:#fff; cursor:pointer;"">Trails: ON</button> | ||||
|             <div style=""display:flex; flex-direction:column; margin-top:4px;""> | ||||
|                 <label for=""trailDuration"" style=""font-size:12px; color:#ccc;"">Trail Duration</label> | ||||
|                 <input type=""range"" id=""trailDuration"" min=""2"" max=""20"" value=""8"" step=""1""> | ||||
|                 <span id=""trailDurationValue"" style=""font-size:12px; color:#ffcc00;"">8s</span> | ||||
|             </div> | ||||
|             <div style=""display:flex; flex-direction:column; margin-top:4px;""> | ||||
|                 <label for=""trailColor"" style=""font-size:12px; color:#ccc;"">Trail Color</label> | ||||
|                 <select id=""trailColor"" style=""padding:2px 4px; background:#333; color:#fff; border:none; border-radius:4px;""> | ||||
|                     <option value='auto'>Auto (By Direction)</option> | ||||
|                     <option value='red'>Red</option> | ||||
|                     <option value='blue'>Blue</option> | ||||
|                     <option value='yellow'>Yellow</option> | ||||
|                     <option value='white'>White</option> | ||||
|                 </select> | ||||
|             </div> | ||||
|             <div id=""topConnections""><b>Top Connections:</b><br>No data yet</div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| <div id=""historicalMapContainer"" style=""display:none;""> | ||||
|     <div id=""histMap"" style=""height:100%; width:100%;""></div> | ||||
|     <div id=""histControls""> | ||||
|         <button id=""playPauseBtn"">Play</button> | ||||
|         <input type=""range"" id=""timeSlider"" min=""0"" max=""0"" value=""0""> | ||||
|         <span id=""timeLabel"">0/0</span> | ||||
|     </div> | ||||
|     <div id=""histStatsPanel""> | ||||
|         <div id=""histStatsHeader"" style=""font-weight:bold; cursor:pointer; padding:4px; background:#333; border-radius:4px;"">Stats</div> | ||||
|         <div id=""histStatsContent"" style=""margin-top:6px;""> | ||||
|             <div id=""histStatsText"">Connections: 0 | Top Country: N/A</div> | ||||
|             <div id=""histTopConnections""><b>Top Connections:</b><br>No data yet</div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| <script src='https://unpkg.com/leaflet@1.9.4/dist/leaflet.js'></script> | ||||
| <script src='https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js'></script> | ||||
| <script src='https://cdnjs.cloudflare.com/ajax/libs/leaflet-polylinedecorator/1.1.0/leaflet.polylineDecorator.min.js'></script> | ||||
| <script src='https://unpkg.com/leaflet.heat/dist/leaflet-heat.js'></script> | ||||
|  | ||||
| <script> | ||||
| /* ------------------- TAB SWITCH ------------------- */ | ||||
| const liveTabBtn = document.getElementById('liveTabBtn'); | ||||
| const histTabBtn = document.getElementById('histTabBtn'); | ||||
| const liveMapContainer = document.getElementById('liveMapContainer'); | ||||
| const historicalMapContainer = document.getElementById('historicalMapContainer'); | ||||
| const histConnectionKeys = new Set(); | ||||
|  | ||||
| liveTabBtn.addEventListener('click', ()=>{ | ||||
|     liveTabBtn.classList.add('active'); histTabBtn.classList.remove('active'); | ||||
|     liveMapContainer.style.display='block'; | ||||
|     historicalMapContainer.style.display='none'; | ||||
|     map.invalidateSize(); | ||||
|     ensureHeatLayer(); | ||||
| }); | ||||
|  | ||||
| histTabBtn.addEventListener('click', ()=>{ | ||||
|     histTabBtn.classList.add('active'); liveTabBtn.classList.remove('active'); | ||||
|     liveMapContainer.style.display='none'; | ||||
|     historicalMapContainer.style.display='block'; | ||||
|     histMap.invalidateSize(); | ||||
| }); | ||||
|  | ||||
| /* ------------------- LIVE MAP ------------------- */ | ||||
| const canvasRenderer = L.canvas({ padding: 0.5 }); | ||||
| const map = L.map('map', { worldCopyJump:true, zoomControl:false }).setView([20,0],2); | ||||
| L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png').addTo(map); | ||||
|  | ||||
| const logoControl = L.control({ position: 'bottomright' }); | ||||
| logoControl.onAdd = function(map) { | ||||
|     const div = L.DomUtil.create('div', 'leaflet-logo'); | ||||
|     div.innerHTML = `<img src=""https://eonacat.com/images/logo.svg"" style=""width:80px; height:auto;"">`; | ||||
|     return div; | ||||
| }; | ||||
|  | ||||
| logoControl.addTo(map); | ||||
|  | ||||
| const markers = L.markerClusterGroup({ disableClusteringAtZoom:6, iconCreateFunction: cluster => { | ||||
|     const count = cluster.getAllChildMarkers().reduce((sum,m)=>sum+(m.options.count||1),0); | ||||
|     return L.divIcon({ html:`<div style='background:#ff5500;border-radius:50%;width:40px;height:40px;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:bold;'>${count}</div>`, className:'' }); | ||||
| }}); | ||||
| map.addLayer(markers); | ||||
|  | ||||
| const lines = L.layerGroup().addTo(map); | ||||
| const trails = L.layerGroup({ renderer: canvasRenderer }).addTo(map); | ||||
| let localMarker=null; | ||||
| const markerMap=new Map(); | ||||
| let trailsEnabled=true, trailDuration=8000, trailColorMode='auto'; | ||||
| let heatLayer; | ||||
|  | ||||
| /* ------------------- STATS PANEL ------------------- */ | ||||
| const statsPanel=document.getElementById('statsPanel'); | ||||
| const statsHeader=document.getElementById('statsHeader'); | ||||
| let isDragging=false, offsetX=0, offsetY=0; | ||||
| statsHeader.addEventListener('mousedown', e=>{ isDragging=true; offsetX=e.clientX-statsPanel.offsetLeft; offsetY=e.clientY-statsPanel.offsetTop; statsPanel.style.cursor='grabbing'; }); | ||||
| document.addEventListener('mousemove', e=>{ if(!isDragging) return; statsPanel.style.left=e.clientX-offsetX+'px'; statsPanel.style.top=e.clientY-offsetY+'px'; }); | ||||
| document.addEventListener('mouseup', ()=>{ if(isDragging){ isDragging=false; statsPanel.style.cursor='default'; } }); | ||||
| statsHeader.addEventListener('click', ()=>{ if(isDragging) return; statsPanel.classList.toggle('collapsed'); }); | ||||
|  | ||||
| document.getElementById('trailColor').addEventListener('change', e=>trailColorMode=e.target.value); | ||||
| document.getElementById('trailDuration').addEventListener('input', e=>{ | ||||
|     trailDuration=parseInt(e.target.value)*1000; | ||||
|     document.getElementById('trailDurationValue').innerText=e.target.value+'s'; | ||||
| }); | ||||
| document.getElementById('toggleTrailsBtn').addEventListener('click', ()=>{ | ||||
|     trailsEnabled=!trailsEnabled; | ||||
|     document.getElementById('toggleTrailsBtn').innerText='Trails: '+(trailsEnabled?'ON':'OFF'); | ||||
|     if(!trailsEnabled) trails.clearLayers(); | ||||
| }); | ||||
|  | ||||
| const histStatsPanel = document.getElementById('histStatsPanel'); | ||||
| const histStatsHeader = document.getElementById('histStatsHeader'); | ||||
| histStatsHeader.addEventListener('click', () => { | ||||
|     histStatsPanel.classList.toggle('collapsed'); | ||||
| }); | ||||
|  | ||||
| /* ------------------- HELPER FUNCTIONS ------------------- */ | ||||
| function getPopupHtml(conn){ | ||||
|     const flag=url=>url?`<img src='${url}' width='24' height='16' style='margin-right:6px;border-radius:2px;'>`:''; | ||||
|     const listItems=arr=>arr?.length?`<ul style='margin:0 0 0 16px;padding:0;list-style-type: disc;'>${arr.map(p=>`<li>${p.ProcessName}: ${p.ConnectionCount}</li>`).join('')}</ul>`:''; | ||||
|     return `<div style='min-width:240px;'> | ||||
|         <div style='display:flex;align-items:center;margin-bottom:6px;'>${flag(conn.fromFlagUrl)}<div style='font-weight:bold;color:#ffcc00;'>${conn.fromCountryName||conn.fromCountryCode}</div>${conn.fromIp?`<div style='margin-left:6px;font-size:11px;color:#aaa;'>(${conn.fromIp})</div>`:''}</div> | ||||
|         <div style='text-align:center;margin-bottom:6px;color:#ff5555;font-size:14px;'>↓↑</div> | ||||
|         <div style='display:flex;align-items:center;margin-bottom:8px;'>${flag(conn.toFlagUrl)}<div style='font-weight:bold;color:#66ccff;'>${conn.toCountryName||conn.toCountryCode}</div>${conn.toIp?`<div style='margin-left:6px;font-size:11px;color:#aaa;'>(${conn.toIp})</div>`:''}</div> | ||||
|         <div style='margin-bottom:8px;font-weight:bold;color:#ff88ff;font-size:13px;'>Connections: ${conn.connectionCount||0}</div> | ||||
|         ${conn.fromTopProcesses?.length?`<div style='margin-bottom:6px;'><div style='font-weight:bold;color:#ffcc66;margin-bottom:2px;'>Top Processes at Source:</div>${listItems(conn.fromTopProcesses)}</div>`:''} | ||||
|         ${conn.toTopProcesses?.length?`<div><div style='font-weight:bold;color:#66ccff;margin-bottom:2px;'>Top Processes at Destination:</div>${listItems(conn.toTopProcesses)}</div>`:''} | ||||
|     </div>`; | ||||
| } | ||||
|  | ||||
| function hasValidPopupData(conn){ | ||||
|     return !!((conn.fromCountryName||conn.fromCountryCode)||(conn.toCountryName||conn.toCountryCode)||conn.connectionCount>0||(conn.fromTopProcesses?.length)||(conn.toTopProcesses?.length)); | ||||
| } | ||||
|  | ||||
| function addTrail(from, to, direction, country, speed) { | ||||
|     if (!trailsEnabled || map.getZoom() > 6) return; | ||||
|  | ||||
|     const color = trailColorMode === 'auto' | ||||
|         ? direction === 'inbound' ? 'rgba(255,0,0,0.6)'  | ||||
|         : direction === 'outbound' ? 'rgba(0,150,255,0.6)'  | ||||
|         : 'rgba(255,215,0,0.6)' | ||||
|         : trailColorMode === 'red' ? 'rgba(255,0,0,0.6)' | ||||
|         : trailColorMode === 'blue' ? 'rgba(0,150,255,0.6)' | ||||
|         : trailColorMode === 'yellow' ? 'rgba(255,215,0,0.6)' | ||||
|         : 'rgba(255,255,255,0.6)'; | ||||
|  | ||||
|     // Main polyline (canvas renderer, non-interactive) | ||||
|     const trail = L.polyline([from, to], {  | ||||
|         color, weight: 2, opacity: 0.6,  | ||||
|         renderer: canvasRenderer, interactive: false  | ||||
|     }).addTo(trails); | ||||
|  | ||||
|     // Arrow decorator (SVG) | ||||
|     const decorator = L.polylineDecorator(trail, { | ||||
|         patterns: [{ | ||||
|             offset: '0%', | ||||
|             repeat: '20px', | ||||
|             symbol: L.Symbol.arrowHead({ pixelSize: 8, pathOptions: { color, opacity: 0.8 } }) | ||||
|         }] | ||||
|     }).addTo(trails); | ||||
|  | ||||
|     // Ensure all SVG elements do not block pointer events | ||||
|     decorator.eachLayer(l => { | ||||
|         if (l._path) l._path.style.pointerEvents = 'none'; | ||||
|     }); | ||||
|  | ||||
|     const trailObj = { trail, decorator, startTime: performance.now(), duration: speed || trailDuration }; | ||||
|  | ||||
|     const animate = time => { | ||||
|         const elapsed = time - trailObj.startTime; | ||||
|         const progress = elapsed / trailObj.duration; | ||||
|  | ||||
|         if (progress >= 1) {  | ||||
|             trails.removeLayer(trailObj.trail);  | ||||
|             trails.removeLayer(trailObj.decorator);  | ||||
|             return;  | ||||
|         } | ||||
|  | ||||
|         const opacity = 0.6 * (1 - progress); | ||||
|         trail.setStyle({ opacity }); | ||||
|         decorator.setPatterns([{ | ||||
|             offset: (progress * 100) % 20 + '%', | ||||
|             repeat: '20px', | ||||
|             symbol: L.Symbol.arrowHead({ pixelSize: 8, pathOptions: { color, opacity: opacity * 1.2 } }) | ||||
|         }]); | ||||
|  | ||||
|         // Make sure new decorator layers are non-interactive | ||||
|         decorator.eachLayer(l => { | ||||
|             if (l._path) l._path.style.pointerEvents = 'none'; | ||||
|         }); | ||||
|  | ||||
|         requestAnimationFrame(animate); | ||||
|     }; | ||||
|  | ||||
|     requestAnimationFrame(animate); | ||||
| } | ||||
|  | ||||
|  | ||||
| /* ------------------- HEAT LAYER FIX ------------------- */ | ||||
| function ensureHeatLayer(){ | ||||
|     if(!heatLayer && map._container.offsetHeight>0){ | ||||
|         heatLayer=L.heatLayer([], {radius:25, blur:15, maxZoom:8}).addTo(map); | ||||
|     } | ||||
| } | ||||
|  | ||||
| const histEventKeys = new Set(); | ||||
| /* ------------------- LIVE MAP UPDATE ------------------- */ | ||||
| window.updateMarkersAndLines = (countries, connections, localCoord) => { | ||||
|     try { | ||||
|         const bounds = []; | ||||
|         let totalConnections = 0; | ||||
|         const activeKeys = new Set(); | ||||
|  | ||||
|         // Count total connections | ||||
|         connections?.forEach(conn => { | ||||
|             totalConnections += conn.connectionCount || 1; | ||||
|         }); | ||||
|  | ||||
|         // Update country markers | ||||
|         countries.forEach(item => { | ||||
|             if (!item.lat || !item.lon) return; | ||||
|             const key = item.countryCode; | ||||
|             activeKeys.add(key); | ||||
|  | ||||
|             let marker; | ||||
|             if (!markerMap[key]) { | ||||
|                 marker = item.count > 50 | ||||
|                     ? L.marker([item.lat, item.lon], { icon: L.divIcon({ className: 'pulse-marker' }) }) | ||||
|                     : L.circleMarker([item.lat, item.lon], { | ||||
|                         radius: Math.min(15, Math.max(5, Math.sqrt(item.count))), | ||||
|                         fillColor: '#3388ff', | ||||
|                         color: '#3388ff', | ||||
|                         weight: 1, | ||||
|                         opacity: 0.6, | ||||
|                         fillOpacity: 0.6 | ||||
|                     }); | ||||
|                 markers.addLayer(marker); | ||||
|                 marker.bindPopup(`Total Connections: ${item.count || 0}`); | ||||
|                 markerMap[key] = marker; | ||||
|             } else { | ||||
|                 marker = markerMap[key]; | ||||
|                 if (marker.setRadius) marker.setRadius(Math.min(15, Math.max(5, Math.sqrt(item.count)))); | ||||
|                 marker.bindPopup(`Total Connections: ${item.count || 0}`); | ||||
|             } | ||||
|  | ||||
|             bounds.push([item.lat, item.lon]); | ||||
|         }); | ||||
|  | ||||
|         // Remove markers for inactive countries | ||||
|         for (const key of markerMap.keys()) { | ||||
|             if (!activeKeys.has(key)) { | ||||
|                 markers.removeLayer(markerMap.get(key)); | ||||
|                 markerMap.delete(key); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Heat layer | ||||
|         ensureHeatLayer(); | ||||
|         if (heatLayer) heatLayer.setLatLngs(countries.map(c => [c.lat, c.lon, Math.sqrt(c.count)])); | ||||
|  | ||||
|         // Local marker | ||||
|         if (localCoord && countries.length > 0) { | ||||
|             if (!localMarker) { | ||||
|                 localMarker = L.circleMarker([localCoord.Lat, localCoord.Lon], { | ||||
|                     radius: 12, | ||||
|                     fillColor: '#00ffcc', | ||||
|                     color: '#00ffcc', | ||||
|                     fillOpacity: 0.8 | ||||
|                 }).addTo(map); | ||||
|                 localMarker.bindPopup('You are here!'); | ||||
|             } else { | ||||
|                 localMarker.setLatLng([localCoord.Lat, localCoord.Lon]); | ||||
|                 localMarker.bindPopup('You are here!'); | ||||
|             } | ||||
|             setTimeout(() => { localMarker.openPopup(); }, 50); | ||||
|         } else if (localMarker) { | ||||
|             map.removeLayer(localMarker); | ||||
|             localMarker = null; | ||||
|         } | ||||
|  | ||||
|         // Clear lines | ||||
|         lines.clearLayers(); | ||||
|  | ||||
|         // Track live top connections | ||||
|         const liveConnectionCounts = {}; | ||||
|  | ||||
|         connections?.forEach(conn => { | ||||
|             if (!conn.fromLat || !conn.fromLon || !conn.toLat || !conn.toLon) return; | ||||
|  | ||||
|             // Draw line | ||||
|             const weight = Math.max(2, Math.min(10, 1 + conn.connectionCount / 5)); | ||||
|             const lineColor = conn.connectionCount > 50 ? 'darkred' : 'red'; | ||||
|             const line = L.polyline([[conn.fromLat, conn.fromLon], [conn.toLat, conn.toLon]], { | ||||
|                 color: lineColor, | ||||
|                 weight, | ||||
|                 opacity: 0.6, | ||||
|                 renderer: canvasRenderer, | ||||
|                 interactive: true | ||||
|             }).addTo(lines); | ||||
|  | ||||
|             if (hasValidPopupData(conn)) { | ||||
|                 line.bindPopup(getPopupHtml(conn)); | ||||
|             } | ||||
|  | ||||
|             // Trail animation | ||||
|             const minSpeed = 2000, maxSpeed = 10000; | ||||
|             const speed = Math.max(minSpeed, maxSpeed - (conn.connectionCount * 50)); | ||||
|             let direction = 'mixed'; | ||||
|             if (conn.fromCountryCode && conn.toCountryCode) { | ||||
|                 if (conn.toCountryCode === 'LOCAL') direction = 'inbound'; | ||||
|                 else if (conn.fromCountryCode === 'LOCAL') direction = 'outbound'; | ||||
|             } | ||||
|             addTrail([conn.fromLat, conn.fromLon], [conn.toLat, conn.toLon], direction, conn.fromCountryCode, speed); | ||||
|  | ||||
|             // Track live top connections | ||||
|             if (conn.fromCountryCode && conn.toCountryCode) { | ||||
|                 const key = `${conn.fromCountryCode}→${conn.toCountryCode}`; | ||||
|                 liveConnectionCounts[key] = (liveConnectionCounts[key] || 0) + (conn.connectionCount || 1); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         // Update live stats panel | ||||
|         let topCountry = 'N/A', topCount = 0; | ||||
|         countries.forEach(c => { | ||||
|             if ((c.count || 0) > topCount) { topCount = c.count; topCountry = c.countryCode; } | ||||
|         }); | ||||
|  | ||||
|         document.getElementById('statsText').innerText = | ||||
|             `Connections: ${totalConnections} | Top Country: ${topCountry}`; | ||||
|  | ||||
|         // Live top connections (top 10) | ||||
|         const sortedLiveConnections = Object.entries(liveConnectionCounts) | ||||
|             .sort((a, b) => b[1] - a[1]) | ||||
|             .slice(0, 10); | ||||
|  | ||||
|         let topLiveConnHtml = '<b>Top Connections:</b><br>'; | ||||
|         if (sortedLiveConnections.length) { | ||||
|             topLiveConnHtml += sortedLiveConnections | ||||
|                 .map(([pair, count]) => `${pair}: ${count}`) | ||||
|                 .join('<br>'); | ||||
|         } else { | ||||
|             topLiveConnHtml += 'No data yet'; | ||||
|         } | ||||
|         document.getElementById('topConnections').innerHTML = topLiveConnHtml; | ||||
|  | ||||
|         // ---------------------- | ||||
|         // Track live totals globally for historical map | ||||
|         // ---------------------- | ||||
|         window.liveTotals = { | ||||
|             totalConnections, | ||||
|             topCountry, | ||||
|             topConnections: sortedLiveConnections.reduce((acc, [pair, count]) => { acc[pair] = count; return acc; }, {}) | ||||
|         }; | ||||
|  | ||||
|         // Update historical map events | ||||
|         if (histEvents.length) { | ||||
|             updateHistStats(histEvents.length - 1, true); | ||||
|         } | ||||
|  | ||||
|         // Historical event logging | ||||
|         const round = n => Math.round(n * 10000) / 10000; | ||||
|         connections?.forEach(conn => { | ||||
|             const histKey = `${round(conn.fromLat)},${round(conn.fromLon)}-${round(conn.toLat)},${round(conn.toLon)}`; | ||||
|             if (!histEventKeys.has(histKey)) { | ||||
|                 histEventKeys.add(histKey); | ||||
|  | ||||
|                 histEvents.push({ | ||||
|                     timestamp: Date.now(), | ||||
|                     fromLat: conn.fromLat, | ||||
|                     fromLon: conn.fromLon, | ||||
|                     toLat: conn.toLat, | ||||
|                     toLon: conn.toLon, | ||||
|                     popupHtml: hasValidPopupData(conn) ? getPopupHtml(conn) : undefined, | ||||
|                     trail: {  | ||||
|                         direction: conn.fromCountryCode === 'LOCAL' ? 'outbound' : (conn.toCountryCode === 'LOCAL' ? 'inbound' : 'mixed'), | ||||
|                         speed: Math.max(2000, 10000 - (conn.connectionCount * 50)), | ||||
|                         colorMode: trailColorMode | ||||
|                     }, | ||||
|                     fromCountryCode: conn.fromCountryCode, | ||||
|                     toCountryCode: conn.toCountryCode, | ||||
|                     connectionCount: conn.connectionCount || 1 | ||||
|                 }); | ||||
|  | ||||
|                 timeSlider.max = histEvents.length - 1; | ||||
|                 timeLabel.innerText = `${currentTimeIndex + 1}/${histEvents.length}`; | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|     } catch (e) { | ||||
|         console.error(e); | ||||
|     } | ||||
| }; | ||||
|  | ||||
|  | ||||
|  | ||||
| /* ------------------- HISTORICAL MAP ------------------- */ | ||||
| const histMap = L.map('histMap', { worldCopyJump:true, zoomControl:false }).setView([20,0],2); | ||||
| L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png').addTo(histMap); | ||||
|  | ||||
| const histLogoControl = L.control({ position: 'bottomright' }); | ||||
| histLogoControl.onAdd = function(map) { | ||||
|     const div = L.DomUtil.create('div', 'leaflet-logo'); | ||||
|     div.innerHTML = `<img src=""https://eonacat.com/images/logo.svg"" style=""width:80px; height:auto;"">`; | ||||
|     return div; | ||||
| }; | ||||
|  | ||||
| histLogoControl.addTo(histMap); | ||||
|  | ||||
| const histMarkers = L.markerClusterGroup({ disableClusteringAtZoom:6 }); | ||||
| const histLines = L.layerGroup().addTo(histMap); | ||||
| const histTrails = L.layerGroup({ renderer: canvasRenderer }).addTo(histMap); | ||||
|  | ||||
| histMap.addLayer(histMarkers); | ||||
| let histEvents = []; | ||||
| const histMarkerMap = new Map(); | ||||
| const histTrailMap = new Map(); | ||||
|  | ||||
| const timeSlider = document.getElementById('timeSlider'); | ||||
| const timeLabel = document.getElementById('timeLabel'); | ||||
| const playPauseBtn = document.getElementById('playPauseBtn'); | ||||
|  | ||||
| /* Load historical data */ | ||||
| function loadHistoricalData(events){ | ||||
|     histEvents = events?.sort((a,b)=>a.timestamp-b.timestamp) || []; | ||||
|     currentTimeIndex = 0; | ||||
|     timeSlider.max = Math.max(histEvents.length - 1, 0); | ||||
|     timeSlider.value = 0; | ||||
|     timeLabel.innerText = `${histEvents.length ? 1 : 0}/${histEvents.length}`; | ||||
|     showHistoricalEvent(currentTimeIndex); | ||||
| } | ||||
|  | ||||
| /* Display a single event on the map */ | ||||
| function showHistoricalEvent(index) { | ||||
|     // Clear map layers | ||||
|     histLines.clearLayers(); | ||||
|     histTrails.clearLayers(); | ||||
|     histTrailMap.clear(); | ||||
|     histMarkerMap.clear(); | ||||
|     histMarkers.clearLayers(); | ||||
|  | ||||
|     // Show all events up to current index | ||||
|     for (let i = 0; i <= index; i++) { | ||||
|         const ev = histEvents[i]; | ||||
|         if (!ev) continue; | ||||
|  | ||||
|         const popupHtml = ev.popupHtml || `From: ${ev.fromLat},${ev.fromLon} <br> To: ${ev.toLat},${ev.toLon}`; | ||||
|  | ||||
|         // From marker | ||||
|         const fromKey = `${ev.fromLat},${ev.fromLon}`; | ||||
|         if (!histMarkerMap.has(fromKey)) { | ||||
|             const markerFrom = L.circleMarker([ev.fromLat, ev.fromLon], { | ||||
|                 radius: 8, fillColor: '#ff5500', color: '#ff5500', fillOpacity: 0.8 | ||||
|             }).bindPopup(popupHtml); | ||||
|             histMarkers.addLayer(markerFrom); | ||||
|             histMarkerMap.set(fromKey, markerFrom); | ||||
|         } | ||||
|  | ||||
|         // To marker | ||||
|         const toKey = `${ev.toLat},${ev.toLon}`; | ||||
|         if (!histMarkerMap.has(toKey)) { | ||||
|             const markerTo = L.circleMarker([ev.toLat, ev.toLon], { | ||||
|                 radius: 8, fillColor: '#55ccff', color: '#55ccff', fillOpacity: 0.8 | ||||
|             }).bindPopup(popupHtml); | ||||
|             histMarkers.addLayer(markerTo); | ||||
|             histMarkerMap.set(toKey, markerTo); | ||||
|         } | ||||
|  | ||||
|         // Line | ||||
|         const line = L.polyline([[ev.fromLat, ev.fromLon], [ev.toLat, ev.toLon]], { | ||||
|             color: '#ffcc00', weight: 2, opacity: 0.7 | ||||
|         }).addTo(histLines); | ||||
|         line.bindPopup(popupHtml); | ||||
|  | ||||
|         // Trail | ||||
|         if (ev.trail && !histTrailMap.has(i)) { | ||||
|             const color = ev.trail.colorMode === 'auto' | ||||
|                 ? ev.trail.direction === 'inbound' ? 'rgba(255,0,0,0.6)' : 'rgba(0,150,255,0.6)' | ||||
|                 : ev.trail.colorMode === 'red' ? 'rgba(255,0,0,0.6)' | ||||
|                 : ev.trail.colorMode === 'blue' ? 'rgba(0,150,255,0.6)' | ||||
|                 : ev.trail.colorMode === 'yellow' ? 'rgba(255,215,0,0.6)' | ||||
|                 : 'rgba(255,255,255,0.6)'; | ||||
|  | ||||
|             const histTrail = L.polyline([[ev.fromLat, ev.fromLon], [ev.toLat, ev.toLon]], { | ||||
|                 color, weight: 2, opacity: 0.6, interactive: false | ||||
|             }).addTo(histTrails); | ||||
|  | ||||
|             const decorator = L.polylineDecorator(histTrail, { | ||||
|                 patterns: [{ offset: '0%', repeat: '20px', | ||||
|                     symbol: L.Symbol.arrowHead({ pixelSize: 8, pathOptions: { color, opacity: 0.8 } }) | ||||
|                 }] | ||||
|             }).addTo(histTrails); | ||||
|  | ||||
|             decorator.eachLayer(l => { if (l._path) l._path.style.pointerEvents = 'none'; }); | ||||
|  | ||||
|             const trailObj = { trail: histTrail, decorator, startTime: performance.now(), duration: ev.trail.speed || trailDuration }; | ||||
|  | ||||
|             const animate = (time) => { | ||||
|                 const elapsed = time - trailObj.startTime; | ||||
|                 const progress = elapsed / trailObj.duration; | ||||
|                 const offsetPercent = (progress * 100) % 20; | ||||
|                 trailObj.decorator.setPatterns([{ | ||||
|                     offset: offsetPercent + '%', | ||||
|                     repeat: '20px', | ||||
|                     symbol: L.Symbol.arrowHead({ pixelSize: 8, pathOptions: { color, opacity: 0.8 } }) | ||||
|                 }]); | ||||
|                 decorator.eachLayer(l => { if (l._path) l._path.style.pointerEvents = 'none'; }); | ||||
|                 requestAnimationFrame(animate); | ||||
|             }; | ||||
|             requestAnimationFrame(animate); | ||||
|  | ||||
|             histTrailMap.set(i, trailObj); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     updateHistStats(index); | ||||
|  | ||||
|     // Fit bounds | ||||
|     const visibleBounds = []; | ||||
|     for (let i = 0; i <= index; i++) { | ||||
|         const ev = histEvents[i]; | ||||
|         if (!ev) continue; | ||||
|         visibleBounds.push([ev.fromLat, ev.fromLon]); | ||||
|         visibleBounds.push([ev.toLat, ev.toLon]); | ||||
|     } | ||||
|     if (visibleBounds.length) { | ||||
|         histMap.fitBounds(L.latLngBounds(visibleBounds).pad(0.25), { maxZoom: 6 }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| function updateHistStats(upToIndex, updateLastFrame) { | ||||
|     let totalConnections, topCountry, sortedConnections; | ||||
|  | ||||
|     if (upToIndex === histEvents.length - 1 && window.liveTotals) { | ||||
|         totalConnections = window.liveTotals.totalConnections; | ||||
|         topCountry = window.liveTotals.topCountry; | ||||
|         sortedConnections = Object.entries(window.liveTotals.topConnections || {}); | ||||
|     } else { | ||||
|         totalConnections = 0; | ||||
|         const countryCounts = {}; | ||||
|         const connectionPairs = {}; | ||||
|         for (let i = 0; i <= upToIndex; i++) { | ||||
|             const ev = histEvents[i]; | ||||
|             if (!ev) continue; | ||||
|             const count = ev.connectionCount || 1; | ||||
|             totalConnections += count; | ||||
|  | ||||
|             const fromCountry = ev.fromCountryCode || `${ev.fromLat},${ev.fromLon}`; | ||||
|             countryCounts[fromCountry] = (countryCounts[fromCountry] || 0) + count; | ||||
|  | ||||
|             const toCountry = ev.toCountryCode || `${ev.toLat},${ev.toLon}`; | ||||
|             const pairKey = `${fromCountry}→${toCountry}`; | ||||
|             connectionPairs[pairKey] = (connectionPairs[pairKey] || 0) + count; | ||||
|         } | ||||
|         topCountry = Object.entries(countryCounts).reduce((a, b) => b[1] > a[1] ? b : a, ['N/A', 0])[0]; | ||||
|         sortedConnections = Object.entries(connectionPairs).sort((a,b)=>b[1]-a[1]).slice(0,10); | ||||
|     } | ||||
|  | ||||
|     // Update the text of the last frame | ||||
|     if (!updateLastFrame || timeSlider.value == histEvents.length - 1) | ||||
|     { | ||||
|         document.getElementById('histStatsText').innerText = | ||||
|             `Connections: ${totalConnections} | Top Country: ${topCountry}`; | ||||
|  | ||||
|         let topConnHtml = '<b>Top Connections:</b><br>'; | ||||
|         if (sortedConnections.length) { | ||||
|             topConnHtml += sortedConnections.map(([pair, count]) => `${pair}: ${count}`).join('<br>'); | ||||
|         } else { | ||||
|             topConnHtml += 'No data yet'; | ||||
|         } | ||||
|         document.getElementById('histTopConnections').innerHTML = topConnHtml; | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| /* Slider control */ | ||||
| timeSlider.addEventListener('input', e => { | ||||
|     currentTimeIndex = parseInt(e.target.value); | ||||
|     showHistoricalEvent(currentTimeIndex); | ||||
|     timeLabel.innerText = `${currentTimeIndex + 1}/${histEvents.length}`; | ||||
| }); | ||||
|  | ||||
| /* Play/Pause control */ | ||||
| let playInterval = null; | ||||
| let isHistPlaying = false; | ||||
| let currentTimeIndex = 0; | ||||
|  | ||||
| playPauseBtn.addEventListener('click', () => { | ||||
|     if (playInterval) { | ||||
|         clearInterval(playInterval); | ||||
|         playInterval = null; | ||||
|         playPauseBtn.innerText = 'Play'; | ||||
|         isHistPlaying = false; | ||||
|     } else { | ||||
|         if (!histEvents.length) return; | ||||
|         playPauseBtn.innerText = 'Pause'; | ||||
|         isHistPlaying = true; | ||||
|  | ||||
|         playInterval = setInterval(() => { | ||||
|             showHistoricalEvent(currentTimeIndex); | ||||
|  | ||||
|             currentTimeIndex++; | ||||
|             if (currentTimeIndex >= histEvents.length) { | ||||
|                 clearInterval(playInterval); | ||||
|                 playInterval = null; | ||||
|                 playPauseBtn.innerText = 'Play'; | ||||
|                 isHistPlaying = false; | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             timeSlider.value = currentTimeIndex; | ||||
|             timeLabel.innerText = `${currentTimeIndex + 1}/${histEvents.length}`; | ||||
|         }, 500); | ||||
|     } | ||||
| }); | ||||
| </script> | ||||
| </body> | ||||
| </html> | ||||
| "; | ||||
|  | ||||
|  | ||||
|         public async Task InitializeMapAsync(WebView2 mapWebView) | ||||
|         { | ||||
|             await mapWebView.EnsureCoreWebView2Async(); | ||||
|  | ||||
|             // Disable default dialogs | ||||
|             mapWebView.CoreWebView2.Settings.AreDefaultScriptDialogsEnabled = false; | ||||
|  | ||||
|             // Disable developer tools and context menu | ||||
|             mapWebView.CoreWebView2.Settings.AreDevToolsEnabled = Debugger.IsAttached; | ||||
|  | ||||
|             // Prevent right-click, F12, Ctrl+Shift+I, Ctrl+R, Ctrl+P | ||||
|             string disableContextAndKeys = @" | ||||
|                     document.addEventListener('contextmenu', e => e.preventDefault()); | ||||
|                     document.addEventListener('keydown', function(e) { | ||||
|                         if ( | ||||
|                             e.key === 'F12' || | ||||
|                             (e.ctrlKey && e.shiftKey && (e.key === 'I' || e.key === 'i')) || | ||||
|                             (e.ctrlKey && (e.key === 'R' || e.key === 'r')) || | ||||
|                             (e.ctrlKey && (e.key === 'P' || e.key === 'p')) | ||||
|                         ) { | ||||
|                             e.preventDefault(); | ||||
|                         } | ||||
|                     }); | ||||
|                 "; | ||||
|  | ||||
|             if (!Debugger.IsAttached) | ||||
|             { | ||||
|                 await mapWebView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(disableContextAndKeys); | ||||
|             } | ||||
|  | ||||
|             // Now navigate to the HTML | ||||
|             mapWebView.NavigateToString(_mapHtml); | ||||
|  | ||||
|             _mapWebView = mapWebView; | ||||
|         } | ||||
|  | ||||
|         public async Task<(double Lat, double Lon)?> GetCountryLatLonAsync(string countryCode) | ||||
|         { | ||||
|             if (string.IsNullOrEmpty(countryCode)) return null; | ||||
|             countryCode = countryCode.Trim().ToUpperInvariant(); | ||||
|             _countryCentroidCache.TryGetValue(countryCode, out var cachedLatLon); | ||||
|  | ||||
|             if (cachedLatLon != default) | ||||
|             { | ||||
|                 return (cachedLatLon.Lat, cachedLatLon.Lon); | ||||
|             } | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 var url = $"https://restcountries.com/v3.1/alpha/{countryCode}"; | ||||
|                 var str = await _mapHttpClient.GetStringAsync(url); | ||||
|                 var root = JArray.Parse(str); | ||||
|                 if (root.Count > 0) | ||||
|                 { | ||||
|                     var latlng = root[0]["latlng"] as JArray; | ||||
|                     var flagUrl = root[0]["flags"]?["png"]?.ToString(); | ||||
|                     if (latlng != null && latlng.Count >= 2) | ||||
|                     { | ||||
|                         var lat = latlng[0].ToObject<double>(); | ||||
|                         var lon = latlng[1].ToObject<double>(); | ||||
|                         _countryCentroidCache[countryCode] = (lat, lon); | ||||
|                         if (flagUrl != null) | ||||
|                         { | ||||
|                             _countryFlagCache[countryCode] = flagUrl; | ||||
|                         } | ||||
|                         return (lat, lon); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             catch { } | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         private async Task<(double Lat, double Lon)?> GetLocalCoordinatesAsync() | ||||
|         { | ||||
|             if (_localCoord != null) return _localCoord; | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 var ipInfo = await _mapHttpClient.GetStringAsync("http://ip-api.com/json"); | ||||
|                 var root = JObject.Parse(ipInfo); | ||||
|                 if (root["status"]?.ToString() == "success") | ||||
|                 { | ||||
|                     _localCoord = (root["lat"].ToObject<double>(), root["lon"].ToObject<double>()); | ||||
|                     return _localCoord; | ||||
|                 } | ||||
|             } | ||||
|             catch { } | ||||
|  | ||||
|             _localCoord = (0.0, 0.0); | ||||
|             return _localCoord; | ||||
|         } | ||||
|  | ||||
|         public async Task UpdateMapAsync( | ||||
|     ObservableCollection<CountryStatistic> countryStats, | ||||
|     List<CountryConnection> connections = null) | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 var localCoord = await GetLocalCoordinatesAsync(); | ||||
|  | ||||
|                 // Prepare payload for countries | ||||
|                 var payloadCountries = new List<object>(); | ||||
|                 foreach (var cs in countryStats) | ||||
|                 { | ||||
|                     if (string.IsNullOrWhiteSpace(cs.CountryCode)) continue; | ||||
|  | ||||
|                     var coord = await GetCountryLatLonAsync(cs.CountryCode); | ||||
|                     if (coord == null) continue; | ||||
|  | ||||
|                     payloadCountries.Add(new | ||||
|                     { | ||||
|                         countryCode = cs.CountryCode, | ||||
|                         countryName = cs.CountryName, | ||||
|                         count = cs.ConnectionCount, | ||||
|                         lat = coord.Value.Lat, | ||||
|                         lon = coord.Value.Lon, | ||||
|                         topProcesses = cs.TopProcesses?.Select(p => new { p.ProcessName, p.ConnectionCount }).ToList(), | ||||
|                         flag = _countryFlagCache.ContainsKey(cs.CountryCode) ? _countryFlagCache[cs.CountryCode] : null | ||||
|                     }); | ||||
|                 } | ||||
|  | ||||
|                 // Prepare payload for connections and track live top connections | ||||
|                 var payloadConnections = new List<object>(); | ||||
|                 var liveConnectionCounts = new Dictionary<string, int>(); | ||||
|  | ||||
|                 foreach (var conn in connections ?? Enumerable.Empty<CountryConnection>()) | ||||
|                 { | ||||
|                     var fromCoord = await GetCountryLatLonAsync(conn.FromCountryCode) ?? localCoord ?? (0.0, 0.0); | ||||
|                     var toCoord = await GetCountryLatLonAsync(conn.ToCountryCode); | ||||
|                     if (toCoord == null) continue; | ||||
|  | ||||
|                     var fromCountry = countryStats.FirstOrDefault(c => c.CountryCode == conn.FromCountryCode); | ||||
|                     var toCountry = countryStats.FirstOrDefault(c => c.CountryCode == conn.ToCountryCode); | ||||
|  | ||||
|                     payloadConnections.Add(new | ||||
|                     { | ||||
|                         fromLat = fromCoord.Lat, | ||||
|                         fromLon = fromCoord.Lon, | ||||
|                         toLat = toCoord.Value.Lat, | ||||
|                         toLon = toCoord.Value.Lon, | ||||
|                         connectionCount = conn.ConnectionCount, | ||||
|                         fromCountryCode = conn.FromCountryCode, | ||||
|                         fromCountryName = fromCountry?.CountryName, | ||||
|                         fromFlagUrl = _countryFlagCache.ContainsKey(conn.FromCountryCode) ? _countryFlagCache[conn.FromCountryCode] : null, | ||||
|                         toFlagUrl = _countryFlagCache.ContainsKey(conn.ToCountryCode) ? _countryFlagCache[conn.ToCountryCode] : null, | ||||
|                         fromIp = conn.FromIp, | ||||
|                         fromTopProcesses = fromCountry?.TopProcesses?.Select(p => new { p.ProcessName, p.ConnectionCount }).ToList(), | ||||
|                         toCountryCode = conn.ToCountryCode, | ||||
|                         toCountryName = toCountry?.CountryName, | ||||
|                         toIp = conn.ToIp, | ||||
|                         toTopProcesses = toCountry?.TopProcesses?.Select(p => new { p.ProcessName, p.ConnectionCount }).ToList() | ||||
|                     }); | ||||
|  | ||||
|                     // Track live top connections | ||||
|                     if (!string.IsNullOrEmpty(conn.FromCountryCode) && !string.IsNullOrEmpty(conn.ToCountryCode)) | ||||
|                     { | ||||
|                         var key = $"{conn.FromCountryCode}→{conn.ToCountryCode}"; | ||||
|                         if (!liveConnectionCounts.ContainsKey(key)) | ||||
|                             liveConnectionCounts[key] = 0; | ||||
|                         liveConnectionCounts[key] += conn.ConnectionCount; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 // Compute live totals | ||||
|                 var totalConnections = countryStats.Sum(c => c.ConnectionCount); | ||||
|  | ||||
|                 var topCountry = countryStats | ||||
|                     .OrderByDescending(c => c.ConnectionCount) | ||||
|                     .FirstOrDefault()?.CountryCode ?? "N/A"; | ||||
|  | ||||
|                 var topConnections = liveConnectionCounts | ||||
|                     .OrderByDescending(kv => kv.Value) | ||||
|                     .Take(10) | ||||
|                     .ToDictionary(kv => kv.Key, kv => kv.Value); | ||||
|  | ||||
|                 // Send minimal payload to WebView2 | ||||
|                 if (_mapWebView?.CoreWebView2 != null) | ||||
|                 { | ||||
|                     var jsonCountries = JsonHelper.ToJson(payloadCountries); | ||||
|                     var jsonConnections = JsonHelper.ToJson(payloadConnections); | ||||
|                     var jsonLiveTotals = JsonHelper.ToJson(new | ||||
|                     { | ||||
|                         totalConnections, | ||||
|                         topCountry, | ||||
|                         topConnections | ||||
|                     }); | ||||
|  | ||||
|                     await _mapWebView.CoreWebView2.ExecuteScriptAsync($@" | ||||
|                 window.updateMarkersAndLines({jsonCountries}, {jsonConnections}); | ||||
|                 window.liveTotals = {jsonLiveTotals}; | ||||
|             "); | ||||
|                 } | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 Debug.WriteLine($"UpdateMapAsync error: {ex.Message}"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,10 +1,13 @@ | ||||
| <Window x:Class="EonaCat.ConnectionMonitor.MainWindow" | ||||
|         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" | ||||
|         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" | ||||
|         Title="EonaCat Connection Monitor" Height="800" Width="1400" | ||||
|         xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf" | ||||
|         xmlns:converters="clr-namespace:EonaCat.ConnectionMonitor.Converters" | ||||
|         Title="EonaCat Connection Monitor" Height="850" Width="1400" | ||||
|         WindowStartupLocation="CenterScreen"> | ||||
|  | ||||
|     <Window.Resources> | ||||
|         <converters:StringToVisibilityConverter x:Key="StringToVisibilityConverter"/> | ||||
|         <Style x:Key="TabItemStyle" TargetType="TabItem"> | ||||
|             <Setter Property="Template"> | ||||
|                 <Setter.Value> | ||||
| @@ -43,10 +46,23 @@ | ||||
|  | ||||
|         <!-- Header --> | ||||
|         <Border Grid.Row="0" Background="#2D2D30" BorderBrush="#3F3F46" BorderThickness="0,0,0,1" Padding="15"> | ||||
|             <StackPanel> | ||||
|                 <TextBlock Text="EonaCat Connection Monitor" FontSize="18" FontWeight="Bold" Foreground="White"/> | ||||
|                 <TextBlock Text="Monitor network connections with process information and geographical data" FontSize="12" Foreground="#CCCCCC" Margin="0,2,0,0"/> | ||||
|             </StackPanel> | ||||
|             <Grid> | ||||
|                 <StackPanel> | ||||
|                     <TextBlock Text="EonaCat Connection Monitor" FontSize="18" FontWeight="Bold" Foreground="White"/> | ||||
|                     <TextBlock Text="Monitor network connections with process information and geographical data" FontSize="12" Foreground="#CCCCCC" Margin="0,2,0,0"/> | ||||
|                 </StackPanel> | ||||
|                 <Button Name="btnDonate" Click="BtnDonate_Click" Width="180" Height="35" Cursor="Hand" HorizontalAlignment="Right"> | ||||
|                     <Button.Template> | ||||
|                         <ControlTemplate TargetType="Button"> | ||||
|                             <Border Background="#003087" BorderBrush="#003087" CornerRadius="4"> | ||||
|                                 <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center"> | ||||
|                                     <TextBlock Text="Donate with PayPal" Foreground="White" FontWeight="Bold" VerticalAlignment="Center"/> | ||||
|                                 </StackPanel> | ||||
|                             </Border> | ||||
|                         </ControlTemplate> | ||||
|                     </Button.Template> | ||||
|                 </Button> | ||||
|             </Grid> | ||||
|         </Border> | ||||
|  | ||||
|         <!-- Controls --> | ||||
| @@ -91,7 +107,7 @@ | ||||
|         </Border> | ||||
|  | ||||
|         <!-- Main Content --> | ||||
|         <TabControl Grid.Row="2" Background="#1E1E1E" BorderBrush="#3F3F46" Style="{x:Null}"> | ||||
|         <TabControl x:Name="tabControl" Grid.Row="2" Background="#1E1E1E" BorderBrush="#3F3F46" Style="{x:Null}"> | ||||
|             <TabItem Header="All Connections" Style="{StaticResource TabItemStyle}"> | ||||
|                 <Grid Background="#1E1E1E"> | ||||
|                     <DataGrid Name="dgAllConnections" | ||||
| @@ -187,8 +203,39 @@ | ||||
|                             <DataGridTextColumn Header="ISP" Binding="{Binding ISP}" Width="150"/> | ||||
|                             <DataGridTextColumn Header="Start Time" Binding="{Binding StartTime, StringFormat=HH:mm:ss}" Width="Auto"/> | ||||
|                             <DataGridTextColumn Header="Duration" Binding="{Binding Duration, StringFormat=c}" Width="Auto"/> | ||||
|                             <DataGridTemplateColumn Header="Actions" Width="80"> | ||||
|                                 <DataGridTemplateColumn.CellTemplate> | ||||
|                                     <DataTemplate> | ||||
|                                         <Button Content="Kill"  | ||||
|                                             Background="#D83B01"  | ||||
|                                             Foreground="White"  | ||||
|                                             Padding="5,2" | ||||
|                                             Cursor="Hand" | ||||
|                                             Click="BtnKillProcess_Click" | ||||
|                                             Tag="{Binding ProcessId}" | ||||
|                                             Visibility="{Binding ProcessName, Converter={StaticResource StringToVisibilityConverter}}"/> | ||||
|                                     </DataTemplate> | ||||
|                                 </DataGridTemplateColumn.CellTemplate> | ||||
|                             </DataGridTemplateColumn> | ||||
|                         </DataGrid.Columns> | ||||
|                     </DataGrid> | ||||
|  | ||||
|                     <Border Background="#80000000" CornerRadius="4" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> | ||||
|                         <Border.Style> | ||||
|                             <Style TargetType="Border"> | ||||
|                                 <Setter Property="Visibility" Value="Collapsed"/> | ||||
|                                 <Style.Triggers> | ||||
|                                     <MultiDataTrigger> | ||||
|                                         <MultiDataTrigger.Conditions> | ||||
|                                             <Condition Binding="{Binding IsLoadingOverlayVisible, RelativeSource={RelativeSource AncestorType=Window}}" Value="True"/> | ||||
|                                         </MultiDataTrigger.Conditions> | ||||
|                                         <Setter Property="Visibility" Value="Visible"/> | ||||
|                                     </MultiDataTrigger> | ||||
|                                 </Style.Triggers> | ||||
|                             </Style> | ||||
|                         </Border.Style> | ||||
|                         <TextBlock Text="Loading..." Foreground="White" FontSize="24" HorizontalAlignment="Center" VerticalAlignment="Center"/> | ||||
|                     </Border> | ||||
|                 </Grid> | ||||
|             </TabItem> | ||||
|  | ||||
| @@ -231,7 +278,24 @@ | ||||
|                             <DataGridTextColumn Header="Port" Binding="{Binding Port}" Width="80"/> | ||||
|                             <DataGridTextColumn Header="Status" Binding="{Binding Status}" Width="100"/> | ||||
|                             <DataGridTextColumn Header="Process" Binding="{Binding ProcessName}" Width="Auto"/> | ||||
|                             <DataGridTextColumn Header="Last Seen" Binding="{Binding LastSeen}" Width="150"/> | ||||
|                             <DataGridTextColumn Header="First Seen" Binding="{Binding StartTime, StringFormat=HH:mm:ss}" Width="Auto"/> | ||||
|                             <DataGridTextColumn Header="Last Seen" Binding="{Binding LastSeen, StringFormat=HH:mm:ss}" Width="Auto"/> | ||||
|                             <DataGridTextColumn Header="Duration" Binding="{Binding Duration, StringFormat=c}" Width="Auto"/> | ||||
|  | ||||
|                             <DataGridTemplateColumn Header="Actions" Width="80"> | ||||
|                                 <DataGridTemplateColumn.CellTemplate> | ||||
|                                     <DataTemplate> | ||||
|                                         <Button Content="Kill"  | ||||
|                                             Background="#D83B01"  | ||||
|                                             Foreground="White"  | ||||
|                                             Padding="5,2" | ||||
|                                             Cursor="Hand" | ||||
|                                             Click="BtnKillProcess_Click" | ||||
|                                             Tag="{Binding ProcessId}" | ||||
|                                             Visibility="{Binding ProcessName, Converter={StaticResource StringToVisibilityConverter}}"/> | ||||
|                                     </DataTemplate> | ||||
|                                 </DataGridTemplateColumn.CellTemplate> | ||||
|                             </DataGridTemplateColumn> | ||||
|                         </DataGrid.Columns> | ||||
|                     </DataGrid> | ||||
|                 </Grid> | ||||
| @@ -331,86 +395,152 @@ | ||||
|                             <DataGridTextColumn Header="Country Name" Binding="{Binding CountryName}" Width="120"/> | ||||
|                             <DataGridTextColumn Header="ISP" Binding="{Binding ISP}" Width="150"/> | ||||
|                             <DataGridTextColumn Header="Start Time" Binding="{Binding StartTime, StringFormat=HH:mm:ss}" Width="Auto"/> | ||||
|                             <DataGridTextColumn Header="End Time" Binding="{Binding EndTime, StringFormat=HH:mm:ss}" Width="Auto"/> | ||||
|                             <DataGridTextColumn Header="End Time" Binding="{Binding LastSeen, StringFormat=HH:mm:ss}" Width="Auto"/> | ||||
|                             <DataGridTextColumn Header="Duration" Binding="{Binding Duration, StringFormat=c}" Width="Auto"/> | ||||
|                             <DataGridTemplateColumn Header="Actions" Width="80"> | ||||
|                                 <DataGridTemplateColumn.CellTemplate> | ||||
|                                     <DataTemplate> | ||||
|                                         <Button Content="Kill"  | ||||
|                                             Background="#D83B01"  | ||||
|                                             Foreground="White"  | ||||
|                                             Padding="5,2" | ||||
|                                             Cursor="Hand" | ||||
|                                             Click="BtnKillProcess_Click" | ||||
|                                             Tag="{Binding ProcessId}" | ||||
|                                             Visibility="{Binding ProcessName, Converter={StaticResource StringToVisibilityConverter}}"/> | ||||
|                                     </DataTemplate> | ||||
|                                 </DataGridTemplateColumn.CellTemplate> | ||||
|                             </DataGridTemplateColumn> | ||||
|                         </DataGrid.Columns> | ||||
|                     </DataGrid> | ||||
|  | ||||
|                     <Border Background="#80000000" CornerRadius="4" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> | ||||
|                         <Border.Style> | ||||
|                             <Style TargetType="Border"> | ||||
|                                 <Setter Property="Visibility" Value="Collapsed"/> | ||||
|                                 <Style.Triggers> | ||||
|                                     <MultiDataTrigger> | ||||
|                                         <MultiDataTrigger.Conditions> | ||||
|                                             <Condition Binding="{Binding Items.Count, ElementName=connectionHistoryDataGrid}" Value="0"/> | ||||
|                                             <Condition Binding="{Binding IsMonitoringStarted, RelativeSource={RelativeSource AncestorType=Window}}" Value="True"/> | ||||
|                                         </MultiDataTrigger.Conditions> | ||||
|                                         <Setter Property="Visibility" Value="Visible"/> | ||||
|                                     </MultiDataTrigger> | ||||
|                                 </Style.Triggers> | ||||
|                             </Style> | ||||
|                         </Border.Style> | ||||
|                         <TextBlock Text="Loading..." Foreground="White" FontSize="24" HorizontalAlignment="Center" VerticalAlignment="Center"/> | ||||
|                     </Border> | ||||
|                 </Grid> | ||||
|             </TabItem> | ||||
|  | ||||
|             <TabItem Header="Connection Events" Style="{StaticResource TabItemStyle}"> | ||||
|                 <DataGrid x:Name="dgConnectionEvents" AutoGenerateColumns="False" | ||||
|                 <Grid Background="#1E1E1E"> | ||||
|                     <DataGrid x:Name="dgConnectionEvents" AutoGenerateColumns="False" | ||||
|               Background="#1E1E1E" Foreground="White" | ||||
|               GridLinesVisibility="Horizontal" HorizontalGridLinesBrush="#3F3F46" | ||||
|               HeadersVisibility="Column" CanUserAddRows="False" IsReadOnly="True" | ||||
|               AlternatingRowBackground="#2D2D30" RowBackground="#1E1E1E"> | ||||
|                     <DataGrid.ColumnHeaderStyle> | ||||
|                         <Style TargetType="DataGridColumnHeader"> | ||||
|                             <Setter Property="Background" Value="#2D2D30"/> | ||||
|                             <Setter Property="Foreground" Value="White"/> | ||||
|                             <Setter Property="FontWeight" Value="Bold"/> | ||||
|                             <Setter Property="Padding" Value="8"/> | ||||
|                             <Setter Property="BorderBrush" Value="#3F3F46"/> | ||||
|                             <Setter Property="BorderThickness" Value="0,0,1,1"/> | ||||
|                         </Style> | ||||
|                     </DataGrid.ColumnHeaderStyle> | ||||
|                     <DataGrid.Columns> | ||||
|                         <DataGridTextColumn Header="Timestamp" Binding="{Binding Timestamp, StringFormat=HH:mm:ss}" Width="Auto"/> | ||||
|                         <DataGridTextColumn Header="Event" Binding="{Binding EventType}" Width="Auto"/> | ||||
|                         <DataGridTextColumn Header="Process" Binding="{Binding ProcessName}" Width="150"/> | ||||
|                         <DataGridTextColumn Header="PID" Binding="{Binding ProcessId}" Width="60"/> | ||||
|                         <DataGridTextColumn Header="Protocol" Binding="{Binding Protocol}" Width="80"/> | ||||
|                         <DataGridTextColumn Header="Local Address" Binding="{Binding LocalEndPoint}" Width="150"/> | ||||
|                         <DataGridTextColumn Header="Remote Address" Binding="{Binding RemoteEndPoint}" Width="150"/> | ||||
|                         <DataGridTextColumn Header="State" Binding="{Binding State}" Width="100"> | ||||
|                             <DataGridTextColumn.ElementStyle> | ||||
|                                 <Style TargetType="TextBlock"> | ||||
|                                     <Setter Property="Background" Value="Transparent"/> | ||||
|                                     <Setter Property="Foreground" Value="White"/> | ||||
|                                     <Setter Property="Padding" Value="4,2"/> | ||||
|                                     <Style.Triggers> | ||||
|                                         <DataTrigger Binding="{Binding State}" Value="ESTABLISHED"> | ||||
|                                             <Setter Property="Background" Value="#218838"/> | ||||
|                                         </DataTrigger> | ||||
|                                         <DataTrigger Binding="{Binding State}" Value="SYN_SENT"> | ||||
|                                             <Setter Property="Background" Value="#E0A800"/> | ||||
|                                         </DataTrigger> | ||||
|                                         <DataTrigger Binding="{Binding State}" Value="SYN_RECEIVED"> | ||||
|                                             <Setter Property="Background" Value="#C69500"/> | ||||
|                                         </DataTrigger> | ||||
|                                         <DataTrigger Binding="{Binding State}" Value="FIN_WAIT_1"> | ||||
|                                             <Setter Property="Background" Value="#CC8400"/> | ||||
|                                         </DataTrigger> | ||||
|                                         <DataTrigger Binding="{Binding State}" Value="FIN_WAIT_2"> | ||||
|                                             <Setter Property="Background" Value="#B26B00"/> | ||||
|                                         </DataTrigger> | ||||
|                                         <DataTrigger Binding="{Binding State}" Value="CLOSE_WAIT"> | ||||
|                                             <Setter Property="Background" Value="#D43F00"/> | ||||
|                                         </DataTrigger> | ||||
|                                         <DataTrigger Binding="{Binding State}" Value="CLOSING"> | ||||
|                                             <Setter Property="Background" Value="#E55535"/> | ||||
|                                         </DataTrigger> | ||||
|                                         <DataTrigger Binding="{Binding State}" Value="LAST_ACK"> | ||||
|                                             <Setter Property="Background" Value="#E55535"/> | ||||
|                                         </DataTrigger> | ||||
|                                         <DataTrigger Binding="{Binding State}" Value="TIME_WAIT"> | ||||
|                                             <Setter Property="Background" Value="#B8860B"/> | ||||
|                                         </DataTrigger> | ||||
|                                         <DataTrigger Binding="{Binding State}" Value="CLOSED"> | ||||
|                                             <Setter Property="Background" Value="#5A6268"/> | ||||
|                                         </DataTrigger> | ||||
|                                         <DataTrigger Binding="{Binding State}" Value="LISTENING"> | ||||
|                                             <Setter Property="Background" Value="#138496"/> | ||||
|                                         </DataTrigger> | ||||
|                                         <DataTrigger Binding="{Binding State}" Value="UDP"> | ||||
|                                             <Setter Property="Background" Value="#520DC2"/> | ||||
|                                         </DataTrigger> | ||||
|                                     </Style.Triggers> | ||||
|                                 </Style> | ||||
|                             </DataGridTextColumn.ElementStyle> | ||||
|                         </DataGridTextColumn> | ||||
|                     </DataGrid.Columns> | ||||
|                 </DataGrid> | ||||
|                         <DataGrid.ColumnHeaderStyle> | ||||
|                             <Style TargetType="DataGridColumnHeader"> | ||||
|                                 <Setter Property="Background" Value="#2D2D30"/> | ||||
|                                 <Setter Property="Foreground" Value="White"/> | ||||
|                                 <Setter Property="FontWeight" Value="Bold"/> | ||||
|                                 <Setter Property="Padding" Value="8"/> | ||||
|                                 <Setter Property="BorderBrush" Value="#3F3F46"/> | ||||
|                                 <Setter Property="BorderThickness" Value="0,0,1,1"/> | ||||
|                             </Style> | ||||
|                         </DataGrid.ColumnHeaderStyle> | ||||
|                         <DataGrid.Columns> | ||||
|                             <DataGridTextColumn Header="Timestamp" Binding="{Binding Timestamp, StringFormat=HH:mm:ss}" Width="Auto"/> | ||||
|                             <DataGridTextColumn Header="Event" Binding="{Binding EventType}" Width="Auto"/> | ||||
|                             <DataGridTextColumn Header="Process" Binding="{Binding ProcessName}" Width="150"/> | ||||
|                             <DataGridTextColumn Header="PID" Binding="{Binding ProcessId}" Width="60"/> | ||||
|                             <DataGridTextColumn Header="Protocol" Binding="{Binding Protocol}" Width="80"/> | ||||
|                             <DataGridTextColumn Header="Local Address" Binding="{Binding LocalEndPoint}" Width="150"/> | ||||
|                             <DataGridTextColumn Header="Remote Address" Binding="{Binding RemoteEndPoint}" Width="150"/> | ||||
|                             <DataGridTextColumn Header="State" Binding="{Binding State}" Width="100"> | ||||
|                                 <DataGridTextColumn.ElementStyle> | ||||
|                                     <Style TargetType="TextBlock"> | ||||
|                                         <Setter Property="Background" Value="Transparent"/> | ||||
|                                         <Setter Property="Foreground" Value="White"/> | ||||
|                                         <Setter Property="Padding" Value="4,2"/> | ||||
|                                         <Style.Triggers> | ||||
|                                             <DataTrigger Binding="{Binding State}" Value="ESTABLISHED"> | ||||
|                                                 <Setter Property="Background" Value="#218838"/> | ||||
|                                             </DataTrigger> | ||||
|                                             <DataTrigger Binding="{Binding State}" Value="SYN_SENT"> | ||||
|                                                 <Setter Property="Background" Value="#E0A800"/> | ||||
|                                             </DataTrigger> | ||||
|                                             <DataTrigger Binding="{Binding State}" Value="SYN_RECEIVED"> | ||||
|                                                 <Setter Property="Background" Value="#C69500"/> | ||||
|                                             </DataTrigger> | ||||
|                                             <DataTrigger Binding="{Binding State}" Value="FIN_WAIT_1"> | ||||
|                                                 <Setter Property="Background" Value="#CC8400"/> | ||||
|                                             </DataTrigger> | ||||
|                                             <DataTrigger Binding="{Binding State}" Value="FIN_WAIT_2"> | ||||
|                                                 <Setter Property="Background" Value="#B26B00"/> | ||||
|                                             </DataTrigger> | ||||
|                                             <DataTrigger Binding="{Binding State}" Value="CLOSE_WAIT"> | ||||
|                                                 <Setter Property="Background" Value="#D43F00"/> | ||||
|                                             </DataTrigger> | ||||
|                                             <DataTrigger Binding="{Binding State}" Value="CLOSING"> | ||||
|                                                 <Setter Property="Background" Value="#E55535"/> | ||||
|                                             </DataTrigger> | ||||
|                                             <DataTrigger Binding="{Binding State}" Value="LAST_ACK"> | ||||
|                                                 <Setter Property="Background" Value="#E55535"/> | ||||
|                                             </DataTrigger> | ||||
|                                             <DataTrigger Binding="{Binding State}" Value="TIME_WAIT"> | ||||
|                                                 <Setter Property="Background" Value="#B8860B"/> | ||||
|                                             </DataTrigger> | ||||
|                                             <DataTrigger Binding="{Binding State}" Value="CLOSED"> | ||||
|                                                 <Setter Property="Background" Value="#5A6268"/> | ||||
|                                             </DataTrigger> | ||||
|                                             <DataTrigger Binding="{Binding State}" Value="LISTENING"> | ||||
|                                                 <Setter Property="Background" Value="#138496"/> | ||||
|                                             </DataTrigger> | ||||
|                                             <DataTrigger Binding="{Binding State}" Value="UDP"> | ||||
|                                                 <Setter Property="Background" Value="#520DC2"/> | ||||
|                                             </DataTrigger> | ||||
|                                         </Style.Triggers> | ||||
|                                     </Style> | ||||
|                                 </DataGridTextColumn.ElementStyle> | ||||
|                             </DataGridTextColumn> | ||||
|                             <DataGridTemplateColumn Header="Actions" Width="80"> | ||||
|                                 <DataGridTemplateColumn.CellTemplate> | ||||
|                                     <DataTemplate> | ||||
|                                         <Button Content="Kill"  | ||||
|                                             Background="#D83B01"  | ||||
|                                             Foreground="White"  | ||||
|                                             Padding="5,2" | ||||
|                                             Cursor="Hand" | ||||
|                                             Click="BtnKillProcess_Click" | ||||
|                                             Tag="{Binding ProcessId}" | ||||
|                                             Visibility="{Binding ProcessName, Converter={StaticResource StringToVisibilityConverter}}"/> | ||||
|                                     </DataTemplate> | ||||
|                                 </DataGridTemplateColumn.CellTemplate> | ||||
|                             </DataGridTemplateColumn> | ||||
|                         </DataGrid.Columns> | ||||
|                     </DataGrid> | ||||
|  | ||||
|                     <Border Background="#80000000" CornerRadius="4" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> | ||||
|                         <Border.Style> | ||||
|                             <Style TargetType="Border"> | ||||
|                                 <Setter Property="Visibility" Value="Collapsed"/> | ||||
|                                 <Style.Triggers> | ||||
|                                     <MultiDataTrigger> | ||||
|                                         <MultiDataTrigger.Conditions> | ||||
|                                             <Condition Binding="{Binding Items.Count, ElementName=dgConnectionEvents}" Value="0"/> | ||||
|                                             <Condition Binding="{Binding IsMonitoringStarted, RelativeSource={RelativeSource AncestorType=Window}}" Value="True"/> | ||||
|                                         </MultiDataTrigger.Conditions> | ||||
|                                         <Setter Property="Visibility" Value="Visible"/> | ||||
|                                     </MultiDataTrigger> | ||||
|                                 </Style.Triggers> | ||||
|                             </Style> | ||||
|                         </Border.Style> | ||||
|                         <TextBlock Text="Loading..." Foreground="White" FontSize="24" HorizontalAlignment="Center" VerticalAlignment="Center"/> | ||||
|                     </Border> | ||||
|                 </Grid> | ||||
|             </TabItem> | ||||
|  | ||||
|             <TabItem Header="Statistics" Style="{StaticResource TabItemStyle}"> | ||||
| @@ -450,59 +580,61 @@ | ||||
|                         </Border> | ||||
|                     </StackPanel> | ||||
|  | ||||
|                     <ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto"> | ||||
|                         <StackPanel> | ||||
|                             <TextBlock Text="Top Processes by Connection Count" Style="{StaticResource HeaderTextStyle}" Margin="0,0,0,10"/> | ||||
|                             <DataGrid Name="dgProcessStats" AutoGenerateColumns="False"  | ||||
|                     <StackPanel Grid.Row="1"> | ||||
|                         <TextBlock Text="Top Processes by Connection Count" Style="{StaticResource HeaderTextStyle}" Margin="0,0,0,10"/> | ||||
|                         <DataGrid Name="dgProcessStats" AutoGenerateColumns="False"  | ||||
|                                       Background="#1E1E1E" Foreground="White" Height="200" | ||||
|                                       GridLinesVisibility="Horizontal" HorizontalGridLinesBrush="#3F3F46" | ||||
|                                       HeadersVisibility="Column" CanUserAddRows="False" IsReadOnly="True" | ||||
|                                       AlternatingRowBackground="#2D2D30" RowBackground="#1E1E1E"> | ||||
|                                 <DataGrid.ColumnHeaderStyle> | ||||
|                                     <Style TargetType="DataGridColumnHeader"> | ||||
|                                         <Setter Property="Background" Value="#2D2D30"/> | ||||
|                                         <Setter Property="Foreground" Value="White"/> | ||||
|                                         <Setter Property="FontWeight" Value="Bold"/> | ||||
|                                         <Setter Property="Padding" Value="8"/> | ||||
|                                     </Style> | ||||
|                                 </DataGrid.ColumnHeaderStyle> | ||||
|                                 <DataGrid.Columns> | ||||
|                                     <DataGridTextColumn Header="Process Name" Binding="{Binding ProcessName}" Width="200"/> | ||||
|                                     <DataGridTextColumn Header="Connection Count" Binding="{Binding ConnectionCount}" Width="120"/> | ||||
|                                     <DataGridTextColumn Header="Unique IPs" Binding="{Binding UniqueIPs}" Width="100"/> | ||||
|                                 </DataGrid.Columns> | ||||
|                             </DataGrid> | ||||
|                             <DataGrid.ColumnHeaderStyle> | ||||
|                                 <Style TargetType="DataGridColumnHeader"> | ||||
|                                     <Setter Property="Background" Value="#2D2D30"/> | ||||
|                                     <Setter Property="Foreground" Value="White"/> | ||||
|                                     <Setter Property="FontWeight" Value="Bold"/> | ||||
|                                     <Setter Property="Padding" Value="8"/> | ||||
|                                 </Style> | ||||
|                             </DataGrid.ColumnHeaderStyle> | ||||
|                             <DataGrid.Columns> | ||||
|                                 <DataGridTextColumn Header="Process Name" Binding="{Binding ProcessName}" Width="200"/> | ||||
|                                 <DataGridTextColumn Header="Connection Count" Binding="{Binding ConnectionCount}" Width="120"/> | ||||
|                                 <DataGridTextColumn Header="Unique IPs" Binding="{Binding UniqueIPs}" Width="100"/> | ||||
|                             </DataGrid.Columns> | ||||
|                         </DataGrid> | ||||
|  | ||||
|                             <TextBlock Text="Countries by Connection Count" Style="{StaticResource HeaderTextStyle}" Margin="0,20,0,10"/> | ||||
|                             <DataGrid Name="dgCountryStats" AutoGenerateColumns="False"  | ||||
|                                       Background="#1E1E1E" Foreground="White" Height="200" | ||||
|                         <TextBlock Text="Countries by Connection Count" Style="{StaticResource HeaderTextStyle}" Margin="0,20,0,10"/> | ||||
|                         <DataGrid Name="dgCountryStats" AutoGenerateColumns="False"  | ||||
|                                       Background="#1E1E1E" Foreground="White" Height="177" | ||||
|                                       GridLinesVisibility="Horizontal" HorizontalGridLinesBrush="#3F3F46" | ||||
|                                       HeadersVisibility="Column" CanUserAddRows="False" IsReadOnly="True" | ||||
|                                       AlternatingRowBackground="#2D2D30" RowBackground="#1E1E1E"> | ||||
|                                 <DataGrid.ColumnHeaderStyle> | ||||
|                                     <Style TargetType="DataGridColumnHeader"> | ||||
|                                         <Setter Property="Background" Value="#2D2D30"/> | ||||
|                                         <Setter Property="Foreground" Value="White"/> | ||||
|                                         <Setter Property="FontWeight" Value="Bold"/> | ||||
|                                         <Setter Property="Padding" Value="8"/> | ||||
|                                     </Style> | ||||
|                                 </DataGrid.ColumnHeaderStyle> | ||||
|                                 <DataGrid.Columns> | ||||
|                                     <DataGridTemplateColumn Header="Flag" Width="60"> | ||||
|                                         <DataGridTemplateColumn.CellTemplate> | ||||
|                                             <DataTemplate> | ||||
|                                                 <Image Source="{Binding FlagUrl}" Width="24" Height="16"/> | ||||
|                                             </DataTemplate> | ||||
|                                         </DataGridTemplateColumn.CellTemplate> | ||||
|                                     </DataGridTemplateColumn> | ||||
|                                     <DataGridTextColumn Header="Country" Binding="{Binding CountryName}" Width="150"/> | ||||
|                                     <DataGridTextColumn Header="Connections" Binding="{Binding ConnectionCount}" Width="100"/> | ||||
|                                 </DataGrid.Columns> | ||||
|                             </DataGrid> | ||||
|                         </StackPanel> | ||||
|                     </ScrollViewer> | ||||
|                             <DataGrid.ColumnHeaderStyle> | ||||
|                                 <Style TargetType="DataGridColumnHeader"> | ||||
|                                     <Setter Property="Background" Value="#2D2D30"/> | ||||
|                                     <Setter Property="Foreground" Value="White"/> | ||||
|                                     <Setter Property="FontWeight" Value="Bold"/> | ||||
|                                     <Setter Property="Padding" Value="8"/> | ||||
|                                 </Style> | ||||
|                             </DataGrid.ColumnHeaderStyle> | ||||
|                             <DataGrid.Columns> | ||||
|                                 <DataGridTemplateColumn Header="Flag" Width="60"> | ||||
|                                     <DataGridTemplateColumn.CellTemplate> | ||||
|                                         <DataTemplate> | ||||
|                                             <Image Source="{Binding FlagUrl}" Width="24" Height="16"/> | ||||
|                                         </DataTemplate> | ||||
|                                     </DataGridTemplateColumn.CellTemplate> | ||||
|                                 </DataGridTemplateColumn> | ||||
|                                 <DataGridTextColumn Header="Country" Binding="{Binding CountryName}" Width="150"/> | ||||
|                                 <DataGridTextColumn Header="Connections" Binding="{Binding ConnectionCount}" Width="100"/> | ||||
|                             </DataGrid.Columns> | ||||
|                         </DataGrid> | ||||
|                     </StackPanel> | ||||
|                 </Grid> | ||||
|             </TabItem> | ||||
|  | ||||
|             <TabItem Header="Map" Style="{StaticResource TabItemStyle}"> | ||||
|                 <wv2:WebView2 x:Name="mapWebView" Grid.Row="0" Grid.Column="2" Margin="6" /> | ||||
|             </TabItem> | ||||
|         </TabControl> | ||||
|  | ||||
|         <!-- Status Bar --> | ||||
| @@ -514,6 +646,9 @@ | ||||
|             <StatusBarItem> | ||||
|                 <TextBlock Name="lblLastUpdate" Text="Last Update: Never" Foreground="#CCCCCC"/> | ||||
|             </StatusBarItem> | ||||
|             <StatusBarItem HorizontalAlignment="Right"> | ||||
|                 <TextBlock Name="lblClock" Foreground="#CCCCCC" HorizontalAlignment="Right"/> | ||||
|             </StatusBarItem> | ||||
|         </StatusBar> | ||||
|     </Grid> | ||||
| </Window> | ||||
| @@ -1,25 +1,24 @@ | ||||
| using Microsoft.Win32; | ||||
| using EonaCat.ConnectionMonitor.Helpers; | ||||
| using EonaCat.ConnectionMonitor.Models; | ||||
| using EonaCat.Json; | ||||
| using EonaCat.Json.Linq; | ||||
| using Microsoft.Win32; | ||||
| using System.Collections.Concurrent; | ||||
| using System.Collections.ObjectModel; | ||||
| using System.ComponentModel; | ||||
| using System.Diagnostics; | ||||
| using System.IO; | ||||
| using System.Linq; | ||||
| using System.Net; | ||||
| using System.Net.Http; | ||||
| using System.Net.NetworkInformation; | ||||
| using System.Text.Json; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using System.Windows; | ||||
| using System.Windows.Controls; | ||||
| using System.Windows.Data; | ||||
| using System.Windows.Media.Imaging; | ||||
| using System.Windows.Threading; | ||||
|  | ||||
| namespace EonaCat.ConnectionMonitor | ||||
| { | ||||
|     public partial class MainWindow : Window | ||||
|     public partial class MainWindow : Window, INotifyPropertyChanged | ||||
|     { | ||||
|         private ObservableCollection<ConnectionInfo> _connectionHistory; | ||||
|         private DispatcherTimer _refreshTimer; | ||||
| @@ -31,20 +30,59 @@ namespace EonaCat.ConnectionMonitor | ||||
|         private CollectionViewSource _allConnectionsView; | ||||
|  | ||||
|         private string _configFilePath = "connections_config.json"; | ||||
|         private bool _isMonitoring = false; | ||||
|         private HttpClient _httpClient; | ||||
|         private LeafLetHelper _leafletHelper; | ||||
|         private bool _isRefreshing; | ||||
|  | ||||
|         public int RemoteConnectionCount { get; private set; } | ||||
|  | ||||
|         private readonly ConcurrentDictionary<string, GeolocationInfo> _geoCache = new(); | ||||
|         private string _connectionEventsLogPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ConnectionEvents.log"); | ||||
|  | ||||
|         private bool _isMonitoringStarted; | ||||
|  | ||||
|         public bool IsMonitoringStarted | ||||
|         { | ||||
|             get => _isMonitoringStarted; | ||||
|             set | ||||
|             { | ||||
|                 _isMonitoringStarted = value; | ||||
|                 NotifyPropertyChanged(nameof(IsMonitoringStarted)); | ||||
|                 NotifyPropertyChanged(nameof(IsLoadingOverlayVisible)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public bool IsLoadingOverlayVisible => IsMonitoringStarted && !_allConnections.Any(); | ||||
|         public event PropertyChangedEventHandler PropertyChanged; | ||||
|         protected void NotifyPropertyChanged(string propertyName) | ||||
|         { | ||||
|             PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); | ||||
|         } | ||||
|  | ||||
|         public MainWindow() | ||||
|         { | ||||
|             InitializeComponent(); | ||||
|             StartClock(); | ||||
|             //Icon = BitmapFrame.Create(new Uri("pack://application:,,,/Resources/app_icon.ico", UriKind.Absolute)); | ||||
|             InitializeCollections(); | ||||
|             InitializeTimer(); | ||||
|             _httpClient = new HttpClient(); | ||||
|             _leafletHelper = new LeafLetHelper(); | ||||
|             _leafletHelper.InitializeMapAsync(mapWebView).ConfigureAwait(false); | ||||
|             LoadConfigurationAsync().ConfigureAwait(false); | ||||
|             StartMonitoring(); | ||||
|         } | ||||
|  | ||||
|         private void StartClock() | ||||
|         { | ||||
|             Task.Run(() => | ||||
|             { | ||||
|                 while (true) | ||||
|                 { | ||||
|                     Dispatcher.Invoke(() => lblClock.Text = $"{DateTime.Now:HH:mm:ss}"); | ||||
|                     Thread.Sleep(1000); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         private void InitializeCollections() | ||||
| @@ -65,8 +103,6 @@ namespace EonaCat.ConnectionMonitor | ||||
|             dgConnectionEvents.ItemsSource = _connectionEvents; | ||||
|         } | ||||
|  | ||||
|  | ||||
|  | ||||
|         private void InitializeTimer() | ||||
|         { | ||||
|             _refreshTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(5) }; | ||||
| @@ -78,6 +114,48 @@ namespace EonaCat.ConnectionMonitor | ||||
|             await RefreshConnectionsAsync(); | ||||
|         } | ||||
|  | ||||
|         private void BtnKillProcess_Click(object sender, RoutedEventArgs e) | ||||
|         { | ||||
|             if (sender is Button btn && btn.Tag is int pid) | ||||
|             { | ||||
|                 try | ||||
|                 { | ||||
|                     Process process = Process.GetProcessById(pid); | ||||
|                     if (MessageBox.Show($"Are you sure you want to kill Process{Environment.NewLine}{process.ProcessName} (PID: {pid})?", | ||||
|                     "Confirm Kill", MessageBoxButton.YesNo, MessageBoxImage.Warning) == MessageBoxResult.Yes) | ||||
|                     { | ||||
|                         process.Kill(); | ||||
|                         process.WaitForExit(); | ||||
|  | ||||
|                         MessageBox.Show($"Process {process.ProcessName} (PID: {pid}) has been terminated.", "Process Killed", MessageBoxButton.OK, MessageBoxImage.Information); | ||||
|                         BtnRefresh_Click(null, null); | ||||
|                     } | ||||
|                 } | ||||
|                 catch (Exception ex) | ||||
|                 { | ||||
|                     MessageBox.Show($"Failed to kill process with PID {pid}: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private void BtnDonate_Click(object sender, RoutedEventArgs e) | ||||
|         { | ||||
|             string paypalUrl = "https://paypal.me/EonaCat"; | ||||
|             try | ||||
|             { | ||||
|                 var psi = new ProcessStartInfo | ||||
|                 { | ||||
|                     FileName = paypalUrl, | ||||
|                     UseShellExecute = true | ||||
|                 }; | ||||
|                 Process.Start(psi); | ||||
|             } | ||||
|             catch (System.Exception ex) | ||||
|             { | ||||
|                 MessageBox.Show($"Could not open browser: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private void BtnStart_Click(object sender, RoutedEventArgs e) | ||||
|         { | ||||
|             StartMonitoring(); | ||||
| @@ -100,7 +178,7 @@ namespace EonaCat.ConnectionMonitor | ||||
|  | ||||
|         private void ChkAutoRefresh_Checked(object sender, RoutedEventArgs e) | ||||
|         { | ||||
|             if (_isMonitoring && chkAutoRefresh.IsChecked == true) | ||||
|             if (IsMonitoringStarted && chkAutoRefresh.IsChecked == true) | ||||
|             { | ||||
|                 _refreshTimer.Start(); | ||||
|             } | ||||
| @@ -113,7 +191,7 @@ namespace EonaCat.ConnectionMonitor | ||||
|  | ||||
|         private void StartMonitoring() | ||||
|         { | ||||
|             _isMonitoring = true; | ||||
|             IsMonitoringStarted = true; | ||||
|             btnStart.IsEnabled = false; | ||||
|             btnStop.IsEnabled = true; | ||||
|             lblStatus.Text = "Monitoring..."; | ||||
| @@ -127,7 +205,7 @@ namespace EonaCat.ConnectionMonitor | ||||
|  | ||||
|         private void StopMonitoring() | ||||
|         { | ||||
|             _isMonitoring = false; | ||||
|             IsMonitoringStarted = false; | ||||
|             _refreshTimer.Stop(); | ||||
|             btnStart.IsEnabled = true; | ||||
|             btnStop.IsEnabled = false; | ||||
| @@ -141,7 +219,7 @@ namespace EonaCat.ConnectionMonitor | ||||
|             try | ||||
|             { | ||||
|                 var dict = _geoCache.ToDictionary(k => k.Key, v => v.Value); | ||||
|                 var json = JsonSerializer.Serialize(dict, new JsonSerializerOptions { WriteIndented = true }); | ||||
|                 var json = JsonHelper.ToJson(dict, Formatting.Indented); | ||||
|                 await File.WriteAllTextAsync(_geoCacheFilePath, json); | ||||
|             } | ||||
|             catch { } | ||||
| @@ -149,11 +227,29 @@ namespace EonaCat.ConnectionMonitor | ||||
|  | ||||
|         private async Task RefreshConnectionsAsync() | ||||
|         { | ||||
|             if (_isRefreshing) return; | ||||
|             if (_isRefreshing) | ||||
|                 return; | ||||
|  | ||||
|             _isRefreshing = true; | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 // Load geo cache if not already loaded | ||||
|                 if (_allConnections.Count == 0 && File.Exists(_geoCacheFilePath)) | ||||
|                 { | ||||
|                     var json = await File.ReadAllTextAsync(_geoCacheFilePath); | ||||
|                     var dict = JsonHelper.ToObject<Dictionary<string, GeolocationInfo>>(json); | ||||
|                     if (dict != null) | ||||
|                     { | ||||
|                         foreach (var kvp in dict) | ||||
|                             _geoCache[kvp.Key] = kvp.Value; | ||||
|                     } | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     NotifyPropertyChanged(nameof(IsLoadingOverlayVisible)); | ||||
|                 } | ||||
|  | ||||
|                 Dispatcher.Invoke(() => lblStatus.Text = "Refreshing..."); | ||||
|  | ||||
|                 var connections = await Task.Run(async () => | ||||
| @@ -163,18 +259,12 @@ namespace EonaCat.ConnectionMonitor | ||||
|                     var tcpConnections = ipProps.GetActiveTcpConnections(); | ||||
|                     var tcpListeners = ipProps.GetActiveTcpListeners(); | ||||
|                     var udpListeners = ipProps.GetActiveUdpListeners(); | ||||
|  | ||||
|                     var pidToProcessName = Process.GetProcesses().ToDictionary(p => p.Id, p => p.ProcessName); | ||||
|  | ||||
|                     var tasks = new List<Task<ConnectionInfo>>(); | ||||
|  | ||||
|                     tasks.AddRange(tcpConnections.Select(c => | ||||
|                         CreateInfoAsync( | ||||
|                             c.LocalEndPoint, | ||||
|                             c.RemoteEndPoint, | ||||
|                             "TCP", | ||||
|                             FormatTcpState(c.State), | ||||
|                             pidToProcessName))); | ||||
|                         CreateInfoAsync(c.LocalEndPoint, c.RemoteEndPoint, "TCP", FormatTcpState(c.State), pidToProcessName))); | ||||
|  | ||||
|                     tasks.AddRange(tcpListeners.Select(l => | ||||
|                         CreateInfoAsync(l, null, "TCP", "LISTENING", pidToProcessName))); | ||||
| @@ -187,13 +277,13 @@ namespace EonaCat.ConnectionMonitor | ||||
|                     return result; | ||||
|                 }); | ||||
|  | ||||
|                 // Remove duplicates | ||||
|                 connections = connections | ||||
|                     .GroupBy(c => $"{c.Protocol}|{c.LocalEndPoint}|{c.RemoteEndPoint}|{c.State}") | ||||
|                     .Select(g => g.First()) | ||||
|                     .ToList(); | ||||
|                 // Assign consistent UniqueId | ||||
|                 foreach (var c in connections) | ||||
|                 { | ||||
|                     c.UniqueId = $"{c.Protocol}|{c.LocalEndPoint}|{c.RemoteEndPoint}"; | ||||
|                 } | ||||
|  | ||||
|                 // Geolocation | ||||
|                 // Geolocation for new IPs | ||||
|                 var ipsToFetch = connections | ||||
|                     .Where(c => c.RemoteEndPoint != "N/A") | ||||
|                     .Select(c => c.RemoteEndPoint.Split(':')[0]) | ||||
| @@ -215,6 +305,7 @@ namespace EonaCat.ConnectionMonitor | ||||
|                 }); | ||||
|                 await Task.WhenAll(geoTasks); | ||||
|  | ||||
|                 // Update geolocation data | ||||
|                 foreach (var conn in connections) | ||||
|                 { | ||||
|                     if (conn.RemoteEndPoint != "N/A") | ||||
| @@ -223,71 +314,71 @@ namespace EonaCat.ConnectionMonitor | ||||
|                         if (_geoCache.TryGetValue(ip, out var geo)) | ||||
|                         { | ||||
|                             conn.CountryCode = geo.CountryCode; | ||||
|                             conn.CountryName = geo.CountryName; | ||||
|                             conn.CountryName = FixCountry(geo.CountryName); | ||||
|                             conn.ISP = geo.ISP; | ||||
|                             conn.CountryFlagUrl = GetFlagUrl(geo.CountryCode)?.ToString(); | ||||
|                             conn.Latitude = geo.Latitude; | ||||
|                             conn.Longitude = geo.Longitude; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 // Track new and disconnected connections by state | ||||
|                 var currentKeys = connections | ||||
|                     .Select(c => $"{c.Protocol}|{c.LocalEndPoint}|{c.RemoteEndPoint}|{c.State}") | ||||
|                     .ToHashSet(); | ||||
|                 // Merge with existing connections | ||||
|                 var currentKeys = connections.Select(c => c.UniqueId).ToHashSet(); | ||||
|                 var previousKeys = _allConnections.Select(c => c.UniqueId).ToHashSet(); | ||||
|  | ||||
|                 var previousKeys = _allConnections | ||||
|                     .Select(c => $"{c.Protocol}|{c.LocalEndPoint}|{c.RemoteEndPoint}|{c.State}") | ||||
|                     .ToHashSet(); | ||||
|  | ||||
|                 // New connections | ||||
|                 var newConnections = connections.Where(c => !previousKeys.Contains( | ||||
|                     $"{c.Protocol}|{c.LocalEndPoint}|{c.RemoteEndPoint}|{c.State}")).ToList(); | ||||
|  | ||||
|                 foreach (var conn in newConnections) | ||||
|                 foreach (var conn in connections) | ||||
|                 { | ||||
|                     conn.StartTime = DateTime.Now; | ||||
|                     conn.LastSeen = DateTime.Now; | ||||
|                     _allConnections.Add(conn); | ||||
|  | ||||
|                     _connectionHistory.Add(new ConnectionInfo | ||||
|                     var existing = _allConnections.FirstOrDefault(c => c.UniqueId == conn.UniqueId); | ||||
|                     if (existing != null) | ||||
|                     { | ||||
|                         ProcessName = conn.ProcessName, | ||||
|                         ProcessId = conn.ProcessId, | ||||
|                         Protocol = conn.Protocol, | ||||
|                         LocalEndPoint = conn.LocalEndPoint, | ||||
|                         RemoteEndPoint = conn.RemoteEndPoint, | ||||
|                         State = /*conn.Protocol == "UDP" ? "UDP" :*/ conn.State.ToUpperInvariant(), | ||||
|                         CountryCode = conn.CountryCode, | ||||
|                         CountryName = conn.CountryName, | ||||
|                         CountryFlagUrl = conn.CountryFlagUrl, | ||||
|                         ISP = conn.ISP, | ||||
|                         StartTime = conn.StartTime, | ||||
|                         LastSeen = conn.LastSeen, | ||||
|                         UniqueId = conn.UniqueId | ||||
|                     }); | ||||
|                     LogConnectionEvent(conn, $"Connected ({conn.State})"); | ||||
|                 } | ||||
|  | ||||
|                 // Disconnected or state-changed connections | ||||
|                 foreach (var conn in _allConnections) | ||||
|                 { | ||||
|                     // Update LastSeen for duration calculation | ||||
|                     conn.LastSeen = DateTime.Now; | ||||
|  | ||||
|                     // Check if connection still exists in current scan | ||||
|                     var key = $"{conn.Protocol}|{conn.LocalEndPoint}|{conn.RemoteEndPoint}|{conn.State}"; | ||||
|                     if (!currentKeys.Contains(key)) | ||||
|                         // Update existing connection info | ||||
|                         existing.LastSeen = DateTime.Now; | ||||
|                         existing.State = conn.State; | ||||
|                         existing.ProcessId = conn.ProcessId; | ||||
|                         existing.ProcessName = conn.ProcessName; | ||||
|                         existing.CountryCode = conn.CountryCode; | ||||
|                         existing.CountryName = conn.CountryName; | ||||
|                         existing.ISP = conn.ISP; | ||||
|                         existing.CountryFlagUrl = conn.CountryFlagUrl; | ||||
|                         existing.Latitude = conn.Latitude; | ||||
|                         existing.Longitude = conn.Longitude; | ||||
|                     } | ||||
|                     else | ||||
|                     { | ||||
|                         LogConnectionEvent(conn, $"Disconnected ({conn.State})"); | ||||
|                         // New connection | ||||
|                         conn.StartTime = DateTime.Now; | ||||
|                         conn.LastSeen = DateTime.Now; | ||||
|                         _allConnections.Add(conn); | ||||
|                         _connectionHistory.Add(new ConnectionInfo | ||||
|                         { | ||||
|                             ProcessName = conn.ProcessName, | ||||
|                             ProcessId = conn.ProcessId, | ||||
|                             Protocol = conn.Protocol, | ||||
|                             LocalEndPoint = conn.LocalEndPoint, | ||||
|                             RemoteEndPoint = conn.RemoteEndPoint, | ||||
|                             State = conn.State, | ||||
|                             CountryCode = conn.CountryCode, | ||||
|                             CountryName = conn.CountryName, | ||||
|                             CountryFlagUrl = conn.CountryFlagUrl, | ||||
|                             ISP = conn.ISP, | ||||
|                             StartTime = conn.StartTime, | ||||
|                             LastSeen = conn.LastSeen, | ||||
|                             UniqueId = conn.UniqueId | ||||
|                         }); | ||||
|                         LogConnectionEvent(conn, $"Connected ({conn.State})"); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 // Remove old connections not in the new list | ||||
|                 // Detect disconnected connections | ||||
|                 for (int i = _allConnections.Count - 1; i >= 0; i--) | ||||
|                 { | ||||
|                     var key = $"{_allConnections[i].Protocol}|{_allConnections[i].LocalEndPoint}|{_allConnections[i].RemoteEndPoint}|{_allConnections[i].State}"; | ||||
|                     if (!currentKeys.Contains(key)) | ||||
|                     var c = _allConnections[i]; | ||||
|                     if (!currentKeys.Contains(c.UniqueId)) | ||||
|                     { | ||||
|                         LogConnectionEvent(c, $"Disconnected ({c.State})"); | ||||
|                         _allConnections.RemoveAt(i); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 // Preserve selection | ||||
| @@ -319,6 +410,8 @@ namespace EonaCat.ConnectionMonitor | ||||
|             finally | ||||
|             { | ||||
|                 _isRefreshing = false; | ||||
|                 RemoteConnectionCount = _allConnections.Count(c => c.RemoteEndPoint != "N/A" && !c.RemoteEndPoint.StartsWith("127.") && !c.RemoteEndPoint.StartsWith("[::1]")); | ||||
|                 UpdateTabHeaders(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -441,14 +534,16 @@ namespace EonaCat.ConnectionMonitor | ||||
|             try | ||||
|             { | ||||
|                 var response = await _httpClient.GetStringAsync($"http://ip-api.com/json/{ipAddress}"); | ||||
|                 var root = JsonDocument.Parse(response).RootElement; | ||||
|                 if (root.GetProperty("status").GetString() == "success") | ||||
|                 var root = JObject.Parse(response); | ||||
|                 if (root["status"]?.ToString() == "success") | ||||
|                 { | ||||
|                     return new GeolocationInfo | ||||
|                     { | ||||
|                         CountryCode = root.GetProperty("countryCode").GetString(), | ||||
|                         CountryName = root.GetProperty("country").GetString(), | ||||
|                         ISP = root.GetProperty("isp").GetString() | ||||
|                         CountryCode = root["countryCode"]?.ToString(), | ||||
|                         CountryName = FixCountry(root["country"]?.ToString()), | ||||
|                         ISP = root["isp"]?.ToString(), | ||||
|                         Latitude = root["lat"]?.ToObject<double>() ?? 0.0, | ||||
|                         Longitude = root["lon"]?.ToObject<double>() ?? 0.0 | ||||
|                     }; | ||||
|                 } | ||||
|             } | ||||
| @@ -456,6 +551,20 @@ namespace EonaCat.ConnectionMonitor | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         private string FixCountry(string? country) | ||||
|         { | ||||
|             if (string.IsNullOrEmpty(country)) | ||||
|             { | ||||
|                 return string.Empty; | ||||
|             } | ||||
|  | ||||
|             if (country == "Netherlands") | ||||
|             { | ||||
|                 return "The Netherlands"; | ||||
|             } | ||||
|             return country; | ||||
|         } | ||||
|  | ||||
|         private Uri GetFlagUrl(string countryCode) | ||||
|         { | ||||
|             if (string.IsNullOrEmpty(countryCode) || countryCode == "Local") | ||||
| @@ -477,7 +586,12 @@ namespace EonaCat.ConnectionMonitor | ||||
|                 { | ||||
|                     configured.Status = "Active"; | ||||
|                     configured.ProcessName = match.ProcessName; | ||||
|                     configured.LastSeen = DateTime.Now; | ||||
|  | ||||
|                     if (configured.StartTime == null) | ||||
|                     { | ||||
|                         configured.StartTime = match.StartTime; | ||||
|                     } | ||||
|                     configured.LastSeen = match.LastSeen; | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
| @@ -486,7 +600,36 @@ namespace EonaCat.ConnectionMonitor | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private void UpdateStatistics() | ||||
|         private void UpdateTabHeaders() | ||||
|         { | ||||
|             // All Connections | ||||
|             tabControl.Items.Cast<TabItem>() | ||||
|                 .FirstOrDefault(t => t.Header.ToString().StartsWith("All Connections")) | ||||
|                 ?.SetValue(TabItem.HeaderProperty, $"All Connections ({dgAllConnections.Items.Count}) - Remote Connections: {RemoteConnectionCount}"); | ||||
|  | ||||
|             // Configured Connections | ||||
|             tabControl.Items.Cast<TabItem>() | ||||
|                 .FirstOrDefault(t => t.Header.ToString().StartsWith("Configured Connections")) | ||||
|                 ?.SetValue(TabItem.HeaderProperty, $"Configured Connections ({dgConfiguredConnections.Items.Count})"); | ||||
|  | ||||
|             // Connection History | ||||
|             tabControl.Items.Cast<TabItem>() | ||||
|                 .FirstOrDefault(t => t.Header.ToString().StartsWith("Connection History")) | ||||
|                 ?.SetValue(TabItem.HeaderProperty, $"Connection History ({connectionHistoryDataGrid.Items.Count})"); | ||||
|  | ||||
|             // Connection Events | ||||
|             tabControl.Items.Cast<TabItem>() | ||||
|                 .FirstOrDefault(t => t.Header.ToString().StartsWith("Connection Events")) | ||||
|                 ?.SetValue(TabItem.HeaderProperty, $"Connection Events ({dgConnectionEvents.Items.Count})"); | ||||
|  | ||||
|             // Statistics (optional, e.g., total connections) | ||||
|             tabControl.Items.Cast<TabItem>() | ||||
|                 .FirstOrDefault(t => t.Header.ToString().StartsWith("Statistics")) | ||||
|                 ?.SetValue(TabItem.HeaderProperty, $"Statistics ({dgProcessStats.Items.Count})"); | ||||
|         } | ||||
|  | ||||
|  | ||||
|         private async void UpdateStatistics() | ||||
|         { | ||||
|             lblTotalConnections.Text = _allConnections.Count.ToString(); | ||||
|             lblEstablishedConnections.Text = _allConnections.Count(c => c.State == "ESTABLISHED").ToString(); | ||||
| @@ -495,6 +638,7 @@ namespace EonaCat.ConnectionMonitor | ||||
|                 .Where(c => !string.IsNullOrEmpty(c.CountryCode) && c.CountryCode != "Local") | ||||
|                 .Select(c => c.CountryCode).Distinct().Count().ToString(); | ||||
|  | ||||
|             // Process statistics | ||||
|             _processStats.Clear(); | ||||
|             var processGroups = _allConnections | ||||
|                 .Where(c => !string.IsNullOrEmpty(c.ProcessName)) | ||||
| @@ -514,6 +658,7 @@ namespace EonaCat.ConnectionMonitor | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             // Country statistics with top processes | ||||
|             _countryStats.Clear(); | ||||
|             var countryGroups = _allConnections | ||||
|                 .Where(c => !string.IsNullOrEmpty(c.CountryCode) && c.CountryCode != "Local") | ||||
| @@ -523,16 +668,79 @@ namespace EonaCat.ConnectionMonitor | ||||
|  | ||||
|             foreach (var g in countryGroups) | ||||
|             { | ||||
|                 var topProcesses = _allConnections | ||||
|                     .Where(c => c.CountryCode == g.Key.CountryCode) | ||||
|                     .GroupBy(c => c.ProcessName) | ||||
|                     .OrderByDescending(pg => pg.Count()) | ||||
|                     .Take(3) | ||||
|                     .Select(pg => new CountryProcess { ProcessName = pg.Key, ConnectionCount = pg.Count() }) | ||||
|                     .ToList(); | ||||
|  | ||||
|                 _countryStats.Add(new CountryStatistic | ||||
|                 { | ||||
|                     CountryCode = g.Key.CountryCode, | ||||
|                     CountryName = g.Key.CountryName, | ||||
|                     CountryName = FixCountry(g.Key.CountryName), | ||||
|                     FlagUrl = g.Key.CountryFlagUrl, | ||||
|                     ConnectionCount = g.Count() | ||||
|                     ConnectionCount = g.Count(), | ||||
|                     TopProcesses = topProcesses | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             // Get LOCAL coordinates dynamically | ||||
|             var localCoord = await GetLocalCoordinatesAsync() ?? (Lat: 0.0, Lon: 0.0, null); | ||||
|  | ||||
|             // Create connections for map | ||||
|             var connections = _allConnections | ||||
|                 .Where(c => !string.IsNullOrEmpty(c.CountryCode) && c.CountryCode != "Local") | ||||
|                 .GroupBy(c => new { From = "LOCAL", To = c.CountryCode }) | ||||
|                 .Select(g => new CountryConnection | ||||
|                 { | ||||
|                     FromCountryCode = g.Key.From == "LOCAL" ? localCoord.CountryCode.ToUpper() : "LOCAL", | ||||
|                     ToCountryCode = g.Key.To, | ||||
|                     ConnectionCount = g.Count(), | ||||
|                     FromLat = localCoord.Lat, | ||||
|                     FromLon = localCoord.Lon, | ||||
|                     FromFlag = GetFlagUrl(localCoord.CountryCode).ToString(), | ||||
|                     ToFlag = g.First().CountryFlagUrl, | ||||
|                     ToLat = g.First().Latitude, | ||||
|                     ToLon = g.First().Longitude, | ||||
|                     ToIp = g.First().RemoteEndPoint.Split(':')[0], | ||||
|                     FromIp = "Local", | ||||
|                     Processes = g.Select(c => c.ProcessName).Distinct().ToList() | ||||
|                 }) | ||||
|                 .ToList(); | ||||
|  | ||||
|             _ = _leafletHelper.UpdateMapAsync(_countryStats, connections).ConfigureAwait(false); | ||||
|         } | ||||
|  | ||||
|         private async Task<(double Lat, double Lon, string CountryCode)?> GetLocalCoordinatesAsync() | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 using var http = new HttpClient(); | ||||
|                 // Get public IP | ||||
|                 var ip = await http.GetStringAsync("https://api.ipify.org"); | ||||
|                 ip = ip.Trim(); | ||||
|  | ||||
|                 // Get geolocation from IP | ||||
|                 var geoJson = await http.GetStringAsync($"http://ip-api.com/json/{ip}"); | ||||
|                 var root = JObject.Parse(geoJson); | ||||
|                 if (root["status"]?.ToString() == "success") | ||||
|                 { | ||||
|                     double lat = root["lat"].ToObject<double>(); | ||||
|                     double lon = root["lon"].ToObject<double>(); | ||||
|                     string countryCode = root["countryCode"].ToObject<string>(); | ||||
|                     return (lat, lon, countryCode); | ||||
|                 } | ||||
|             } | ||||
|             catch | ||||
|             { | ||||
|                 // fallback | ||||
|             } | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|  | ||||
|         private void ApplyFilter() | ||||
|         { | ||||
|             var filterText = txtFilter.Text ?? string.Empty; | ||||
| @@ -609,12 +817,12 @@ namespace EonaCat.ConnectionMonitor | ||||
|                         new ConfiguredConnection { DisplayName = "Microsoft Update", IpAddress = "13.107.42.14", Port = 443 } | ||||
|                     }; | ||||
|  | ||||
|                     var json = JsonSerializer.Serialize(defaultConfig, new JsonSerializerOptions { WriteIndented = true }); | ||||
|                     var json = JsonHelper.ToJson(defaultConfig, Formatting.Indented); | ||||
|                     await File.WriteAllTextAsync(_configFilePath, json); | ||||
|                 } | ||||
|  | ||||
|                 var configJson = await File.ReadAllTextAsync(_configFilePath); | ||||
|                 var configurations = JsonSerializer.Deserialize<List<ConfiguredConnection>>(configJson); | ||||
|                 var configurations = JsonHelper.ToObject<List<ConfiguredConnection>>(configJson); | ||||
|  | ||||
|                 _configuredConnections.Clear(); | ||||
|                 foreach (var config in configurations) | ||||
| @@ -635,7 +843,7 @@ namespace EonaCat.ConnectionMonitor | ||||
|             try | ||||
|             { | ||||
|                 var configurations = _configuredConnections.ToList(); | ||||
|                 var json = JsonSerializer.Serialize(configurations, new JsonSerializerOptions { WriteIndented = true }); | ||||
|                 var json = JsonHelper.ToJson(configurations, Formatting.Indented); | ||||
|                 await File.WriteAllTextAsync(_configFilePath, json); | ||||
|  | ||||
|                 lblStatusBar.Text = $"Configuration saved to {_configFilePath}"; | ||||
| @@ -654,130 +862,4 @@ namespace EonaCat.ConnectionMonitor | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     public class ConnectionInfo : INotifyPropertyChanged | ||||
|     { | ||||
|         public string ProcessName { get; set; } | ||||
|         public int ProcessId { get; set; } | ||||
|         public string Protocol { get; set; } | ||||
|         public string LocalEndPoint { get; set; } | ||||
|         public string RemoteEndPoint { get; set; } | ||||
|         public string State { get; set; } | ||||
|         public string CountryCode { get; set; } | ||||
|         public string CountryName { get; set; } | ||||
|         public string CountryFlagUrl { get; set; } | ||||
|         public string ISP { get; set; } | ||||
|         public string UniqueId { get; set; } | ||||
|  | ||||
|         private DateTime? _startTime; | ||||
|         public DateTime? StartTime | ||||
|         { | ||||
|             get => _startTime; | ||||
|             set | ||||
|             { | ||||
|                 _startTime = value; | ||||
|                 OnPropertyChanged(nameof(StartTime)); | ||||
|                 OnPropertyChanged(nameof(Duration)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private DateTime? _lastSeen; | ||||
|         public DateTime? LastSeen | ||||
|         { | ||||
|             get => _lastSeen; | ||||
|             set | ||||
|             { | ||||
|                 _lastSeen = value; | ||||
|                 OnPropertyChanged(nameof(LastSeen)); | ||||
|                 OnPropertyChanged(nameof(Duration)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public TimeSpan? Duration => (StartTime != null && LastSeen != null) ? LastSeen - StartTime : null; | ||||
|  | ||||
|         public event PropertyChangedEventHandler PropertyChanged; | ||||
|         private void OnPropertyChanged(string propertyName) => | ||||
|             PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); | ||||
|  | ||||
|         private BitmapImage _flagImage; | ||||
|         public BitmapImage CountryFlagImage | ||||
|         { | ||||
|             get | ||||
|             { | ||||
|                 if (_flagImage != null) | ||||
|                 { | ||||
|                     return _flagImage; | ||||
|                 } | ||||
|  | ||||
|                 if (string.IsNullOrEmpty(CountryFlagUrl)) | ||||
|                 { | ||||
|                     return null; | ||||
|                 } | ||||
|  | ||||
|                 try | ||||
|                 { | ||||
|                     var img = new BitmapImage(); | ||||
|                     img.BeginInit(); | ||||
|                     img.UriSource = new Uri(CountryFlagUrl); | ||||
|                     img.CacheOption = BitmapCacheOption.OnLoad; | ||||
|                     img.EndInit(); | ||||
|                     if (img.IsFrozen == false && img.CanFreeze) | ||||
|                     { | ||||
|                         img.Freeze(); | ||||
|                     } | ||||
|                     _flagImage = img; | ||||
|                     return _flagImage; | ||||
|                 } | ||||
|                 catch | ||||
|                 { | ||||
|                     return null; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public class ConnectionEvent | ||||
|     { | ||||
|         public string ProcessName { get; set; } | ||||
|         public int ProcessId { get; set; } | ||||
|         public string Protocol { get; set; } | ||||
|         public string LocalEndPoint { get; set; } | ||||
|         public string RemoteEndPoint { get; set; } | ||||
|         public string EventType { get; set; } // "Connected" or "Disconnected" | ||||
|         public DateTime Timestamp { get; set; } | ||||
|         public string State { get; set; } | ||||
|     } | ||||
|  | ||||
|     public class ConfiguredConnection : INotifyPropertyChanged | ||||
|     { | ||||
|         public string DisplayName { get; set; } | ||||
|         public string IpAddress { get; set; } | ||||
|         public int Port { get; set; } | ||||
|         public string Status { get; set; } = "Unknown"; | ||||
|         public string ProcessName { get; set; } | ||||
|         public DateTime? LastSeen { get; set; } | ||||
|         public event PropertyChangedEventHandler PropertyChanged; | ||||
|     } | ||||
|  | ||||
|     public class ProcessStatistic | ||||
|     { | ||||
|         public string ProcessName { get; set; } | ||||
|         public int ConnectionCount { get; set; } | ||||
|         public int UniqueIPs { get; set; } | ||||
|     } | ||||
|  | ||||
|     public class CountryStatistic | ||||
|     { | ||||
|         public string CountryCode { get; set; } | ||||
|         public string CountryName { get; set; } | ||||
|         public string FlagUrl { get; set; } | ||||
|         public int ConnectionCount { get; set; } | ||||
|     } | ||||
|  | ||||
|     public class GeolocationInfo | ||||
|     { | ||||
|         public string CountryCode { get; set; } | ||||
|         public string CountryName { get; set; } | ||||
|         public string ISP { get; set; } | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										42
									
								
								EonaCat.ConnectionMonitor/Models/ConfiguredConnection.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								EonaCat.ConnectionMonitor/Models/ConfiguredConnection.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| using System.ComponentModel; | ||||
|  | ||||
| namespace EonaCat.ConnectionMonitor.Models | ||||
| { | ||||
|     public class ConfiguredConnection : INotifyPropertyChanged | ||||
|     { | ||||
|         public string DisplayName { get; set; } | ||||
|         public string IpAddress { get; set; } | ||||
|         public int Port { get; set; } | ||||
|         public string Status { get; set; } = "Unknown"; | ||||
|         public string ProcessName { get; set; } | ||||
|         private DateTime? _startTime; | ||||
|         public DateTime? StartTime | ||||
|         { | ||||
|             get => _startTime; | ||||
|             set | ||||
|             { | ||||
|                 _startTime = value; | ||||
|                 OnPropertyChanged(nameof(StartTime)); | ||||
|                 OnPropertyChanged(nameof(Duration)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private DateTime? _lastSeen; | ||||
|         public DateTime? LastSeen | ||||
|         { | ||||
|             get => _lastSeen; | ||||
|             set | ||||
|             { | ||||
|                 _lastSeen = value; | ||||
|                 OnPropertyChanged(nameof(LastSeen)); | ||||
|                 OnPropertyChanged(nameof(Duration)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public TimeSpan? Duration => StartTime != null && LastSeen != null ? LastSeen - StartTime : null; | ||||
|  | ||||
|         public event PropertyChangedEventHandler PropertyChanged; | ||||
|         private void OnPropertyChanged(string propertyName) => | ||||
|     PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										14
									
								
								EonaCat.ConnectionMonitor/Models/ConnectionEvent.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								EonaCat.ConnectionMonitor/Models/ConnectionEvent.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| namespace EonaCat.ConnectionMonitor.Models | ||||
| { | ||||
|     public class ConnectionEvent | ||||
|     { | ||||
|         public string ProcessName { get; set; } | ||||
|         public int ProcessId { get; set; } | ||||
|         public string Protocol { get; set; } | ||||
|         public string LocalEndPoint { get; set; } | ||||
|         public string RemoteEndPoint { get; set; } | ||||
|         public string EventType { get; set; } // "Connected" or "Disconnected" | ||||
|         public DateTime Timestamp { get; set; } | ||||
|         public string State { get; set; } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										89
									
								
								EonaCat.ConnectionMonitor/Models/ConnectionInfo.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								EonaCat.ConnectionMonitor/Models/ConnectionInfo.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,89 @@ | ||||
| using System.ComponentModel; | ||||
| using System.Windows.Media.Imaging; | ||||
|  | ||||
| namespace EonaCat.ConnectionMonitor.Models | ||||
| { | ||||
|     public class ConnectionInfo : INotifyPropertyChanged | ||||
|     { | ||||
|         public string ProcessName { get; set; } | ||||
|         public int ProcessId { get; set; } | ||||
|         public string Protocol { get; set; } | ||||
|         public string LocalEndPoint { get; set; } | ||||
|         public string RemoteEndPoint { get; set; } | ||||
|         public string State { get; set; } | ||||
|         public string CountryCode { get; set; } | ||||
|         public string CountryName { get; set; } | ||||
|         public string CountryFlagUrl { get; set; } | ||||
|         public string ISP { get; set; } | ||||
|         public string UniqueId { get; set; } | ||||
|  | ||||
|         private DateTime? _startTime; | ||||
|         public DateTime? StartTime | ||||
|         { | ||||
|             get => _startTime; | ||||
|             set | ||||
|             { | ||||
|                 _startTime = value; | ||||
|                 OnPropertyChanged(nameof(StartTime)); | ||||
|                 OnPropertyChanged(nameof(Duration)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private DateTime? _lastSeen; | ||||
|         public DateTime? LastSeen | ||||
|         { | ||||
|             get => _lastSeen; | ||||
|             set | ||||
|             { | ||||
|                 _lastSeen = value; | ||||
|                 OnPropertyChanged(nameof(LastSeen)); | ||||
|                 OnPropertyChanged(nameof(Duration)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public TimeSpan? Duration => StartTime != null && LastSeen != null ? LastSeen - StartTime : null; | ||||
|  | ||||
|         public event PropertyChangedEventHandler PropertyChanged; | ||||
|         private void OnPropertyChanged(string propertyName) => | ||||
|             PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); | ||||
|  | ||||
|         private BitmapImage _flagImage; | ||||
|         public BitmapImage CountryFlagImage | ||||
|         { | ||||
|             get | ||||
|             { | ||||
|                 if (_flagImage != null) | ||||
|                 { | ||||
|                     return _flagImage; | ||||
|                 } | ||||
|  | ||||
|                 if (string.IsNullOrEmpty(CountryFlagUrl)) | ||||
|                 { | ||||
|                     return null; | ||||
|                 } | ||||
|  | ||||
|                 try | ||||
|                 { | ||||
|                     var img = new BitmapImage(); | ||||
|                     img.BeginInit(); | ||||
|                     img.UriSource = new Uri(CountryFlagUrl); | ||||
|                     img.CacheOption = BitmapCacheOption.OnLoad; | ||||
|                     img.EndInit(); | ||||
|                     if (img.IsFrozen == false && img.CanFreeze) | ||||
|                     { | ||||
|                         img.Freeze(); | ||||
|                     } | ||||
|                     _flagImage = img; | ||||
|                     return _flagImage; | ||||
|                 } | ||||
|                 catch | ||||
|                 { | ||||
|                     return null; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public double Longitude { get; set; } | ||||
|         public double Latitude { get; set; } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										21
									
								
								EonaCat.ConnectionMonitor/Models/CountryConnection.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								EonaCat.ConnectionMonitor/Models/CountryConnection.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| namespace EonaCat.ConnectionMonitor.Models | ||||
| { | ||||
|     public class CountryConnection | ||||
|     { | ||||
|         public string FromCountryCode { get; set; } | ||||
|         public string ToCountryCode { get; set; } | ||||
|         public string FromFlag { get; set; } | ||||
|         public string ToFlag { get; set; } | ||||
|         public int ConnectionCount { get; set; } | ||||
|  | ||||
|         public double? FromLat { get; set; } | ||||
|         public double? FromLon { get; set; } | ||||
|         public double? ToLat { get; set; } | ||||
|         public double? ToLon { get; set; } | ||||
|         public string ToIp { get; set; } | ||||
|         public string FromIp { get; set; } | ||||
|  | ||||
|  | ||||
|         public List<string> Processes { get; set; } = new List<string>(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										8
									
								
								EonaCat.ConnectionMonitor/Models/CountryProcess.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								EonaCat.ConnectionMonitor/Models/CountryProcess.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| namespace EonaCat.ConnectionMonitor.Models | ||||
| { | ||||
|     public class CountryProcess | ||||
|     { | ||||
|         public string ProcessName { get; set; } | ||||
|         public int ConnectionCount { get; set; } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										11
									
								
								EonaCat.ConnectionMonitor/Models/CountryStatistic.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								EonaCat.ConnectionMonitor/Models/CountryStatistic.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| namespace EonaCat.ConnectionMonitor.Models | ||||
| { | ||||
|     public class CountryStatistic | ||||
|     { | ||||
|         public string CountryCode { get; set; } | ||||
|         public string CountryName { get; set; } | ||||
|         public string FlagUrl { get; set; } | ||||
|         public int ConnectionCount { get; set; } | ||||
|         public List<CountryProcess> TopProcesses { get; set; } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										11
									
								
								EonaCat.ConnectionMonitor/Models/GeolocationInfo.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								EonaCat.ConnectionMonitor/Models/GeolocationInfo.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| namespace EonaCat.ConnectionMonitor.Models | ||||
| { | ||||
|     public class GeolocationInfo | ||||
|     { | ||||
|         public string CountryCode { get; set; } | ||||
|         public string CountryName { get; set; } | ||||
|         public string ISP { get; set; } | ||||
|         public double Longitude { get; set; } | ||||
|         public double Latitude { get; set; } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										9
									
								
								EonaCat.ConnectionMonitor/Models/ProcessStatistic.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								EonaCat.ConnectionMonitor/Models/ProcessStatistic.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| namespace EonaCat.ConnectionMonitor.Models | ||||
| { | ||||
|     public class ProcessStatistic | ||||
|     { | ||||
|         public string ProcessName { get; set; } | ||||
|         public int ConnectionCount { get; set; } | ||||
|         public int UniqueIPs { get; set; } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										8
									
								
								EonaCat.ConnectionMonitor/Models/TopProcessInfo.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								EonaCat.ConnectionMonitor/Models/TopProcessInfo.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| namespace EonaCat.ConnectionMonitor.Models | ||||
| { | ||||
|     public class TopProcessInfo | ||||
|     { | ||||
|         public string ProcessName { get; set; } | ||||
|         public int ConnectionCount { get; set; } | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user