Add WebSocket support for real-time traceroute visualizations.
This commit is contained in:
parent
8fd496c59d
commit
328fb3e842
5 changed files with 543 additions and 23 deletions
|
|
@ -90,6 +90,18 @@
|
|||
border: 1px solid white;
|
||||
}
|
||||
|
||||
.icon-traceroute-start {
|
||||
background-color: #16a34a; /* green */
|
||||
border-radius: 25px;
|
||||
border: 1px solid white;
|
||||
}
|
||||
|
||||
.icon-traceroute-end {
|
||||
background-color: #dc2626; /* red */
|
||||
border-radius: 25px;
|
||||
border: 1px solid white;
|
||||
}
|
||||
|
||||
.waypoint-label {
|
||||
font-size: 26px;
|
||||
background-color: transparent;
|
||||
|
|
@ -1619,7 +1631,7 @@
|
|||
} catch(e) {}
|
||||
|
||||
// overlays enabled by default
|
||||
return ["Legend", "Position History"];
|
||||
return ["Legend", "Position History", "Traceroutes"];
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -2791,6 +2803,16 @@
|
|||
iconSize: [16, 16], // increase from 12px to 16px to make hover easier
|
||||
});
|
||||
|
||||
var iconTracerouteStart = L.divIcon({
|
||||
className: 'icon-traceroute-start',
|
||||
iconSize: [16, 16],
|
||||
});
|
||||
|
||||
var iconTracerouteEnd = L.divIcon({
|
||||
className: 'icon-traceroute-end',
|
||||
iconSize: [16, 16],
|
||||
});
|
||||
|
||||
// create legend
|
||||
var legendLayerGroup = new L.LayerGroup();
|
||||
var legend = L.control({position: 'bottomleft'});
|
||||
|
|
@ -2801,7 +2823,8 @@
|
|||
div.innerHTML = `<div style="margin-bottom:6px;"><strong>Legend</strong></div>`
|
||||
+ `<div style="display:flex"><div class="icon-mediumfast" style="width:12px;height:12px;margin-right:4px;margin-top:auto;margin-bottom:auto;"></div> MediumFast</div>`
|
||||
+ `<div style="display:flex"><div class="icon-longfast" style="width:12px;height:12px;margin-right:4px;margin-top:auto;margin-bottom:auto;"></div> LongFast</div>`
|
||||
+ `<div style="display:flex"><div class="icon-offline" style="width:12px;height:12px;margin-right:4px;margin-top:auto;margin-bottom:auto;"></div> Offline Too Long</div>`;
|
||||
+ `<div style="display:flex"><div class="icon-offline" style="width:12px;height:12px;margin-right:4px;margin-top:auto;margin-bottom:auto;"></div> Offline Too Long</div>`
|
||||
+ `<div style="display:flex"><svg width="16" height="3" style="margin-right:4px;margin-top:auto;margin-bottom:auto;"><line x1="0" y1="1.5" x2="16" y2="1.5" stroke="#f97316" stroke-width="4"/></svg> Traceroute</div>`;
|
||||
return div;
|
||||
};
|
||||
|
||||
|
|
@ -3865,8 +3888,6 @@
|
|||
|
||||
function onTracerouteEdgesUpdated(edges) {
|
||||
|
||||
traceroutesLayerGroup.clearLayers();
|
||||
|
||||
tracerouteEdgesCache = edges;
|
||||
|
||||
for (const edge of edges) {
|
||||
|
|
@ -3910,23 +3931,6 @@
|
|||
+ `<br/><br/>Terrain images from <a href="http://www.heywhatsthat.com" target="_blank">HeyWhatsThat.com</a>`
|
||||
+ `<br/><a href="${terrainImageUrl}" target="_blank"><img src="${terrainImageUrl}" width="100%"></a>`;
|
||||
|
||||
const line = L.polyline([
|
||||
fromMarker.getLatLng(),
|
||||
toMarker.getLatLng(),
|
||||
], {
|
||||
color: colour,
|
||||
opacity: 0.9,
|
||||
}).addTo(traceroutesLayerGroup);
|
||||
|
||||
line.bindTooltip(tooltip, {
|
||||
sticky: true,
|
||||
opacity: 1,
|
||||
interactive: true,
|
||||
}).bindPopup(tooltip)
|
||||
.on('click', function(event) {
|
||||
event.target.closeTooltip();
|
||||
});
|
||||
|
||||
// additional line for backbone neighbours
|
||||
const backboneNeighbourLine = L.polyline([
|
||||
fromMarker.getLatLng(),
|
||||
|
|
@ -3954,7 +3958,6 @@
|
|||
});
|
||||
|
||||
if(fromNode.is_backbone && toNode.is_backbone) {
|
||||
console.log("Adding to backbone neighbours layer group");
|
||||
backboneNeighbourLine.addTo(backboneNeighboursLayerGroup);
|
||||
}
|
||||
}
|
||||
|
|
@ -4328,6 +4331,192 @@
|
|||
// reload and go to provided node id
|
||||
reload(queryNodeId, queryZoom);
|
||||
|
||||
// WebSocket connection for real-time messages
|
||||
var ws = null;
|
||||
var tracerouteCooldown = {}; // Track last traceroute time per from node (for 20s cooldown)
|
||||
var activeTracerouteKeys = new Set(); // Track active traceroute visualizations to prevent duplicates
|
||||
var tracerouteLines = {}; // Track lines for each traceroute route key for cleanup
|
||||
|
||||
function connectWebSocket() {
|
||||
// Determine WebSocket URL - use same hostname as current page, port 8081
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsHost = window.location.hostname;
|
||||
const wsPort = '8081';
|
||||
const wsUrl = `${wsProtocol}//${wsHost}:${wsPort}`;
|
||||
|
||||
console.log('Connecting to WebSocket:', wsUrl);
|
||||
ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onopen = function() {
|
||||
console.log('WebSocket connected');
|
||||
};
|
||||
|
||||
ws.onmessage = function(event) {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
handleWebSocketMessage(message);
|
||||
} catch (e) {
|
||||
console.error('Error parsing WebSocket message:', e);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = function(error) {
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
|
||||
ws.onclose = function() {
|
||||
console.log('WebSocket disconnected, reconnecting in 5 seconds...');
|
||||
setTimeout(connectWebSocket, 5000);
|
||||
};
|
||||
}
|
||||
|
||||
function handleWebSocketMessage(message) {
|
||||
if (message.type === 'traceroute') {
|
||||
handleTraceroute(message.data);
|
||||
}
|
||||
}
|
||||
|
||||
function handleTraceroute(data) {
|
||||
// Only visualize traceroutes where want_response is false (the reply coming back)
|
||||
if (data.want_response) {
|
||||
return;
|
||||
}
|
||||
|
||||
// When want_response is false, from and to are swapped from the original request
|
||||
// The path goes from 'to' (original sender) through route to 'from' (original destination)
|
||||
const originalSender = data.to; // This was the original sender
|
||||
const originalDestination = data.from; // This was the original destination
|
||||
const route = data.route || [];
|
||||
const snrTowards = data.snr_towards || [];
|
||||
|
||||
// Deduplicate: ignore traceroutes from the same original sender for 20 seconds
|
||||
const now = Date.now();
|
||||
if (tracerouteCooldown[originalSender] && (now - tracerouteCooldown[originalSender]) < 20000) {
|
||||
return; // Still in cooldown period
|
||||
}
|
||||
|
||||
// Create unique key for this traceroute path to prevent duplicate visualizations
|
||||
// Use original sender (to), original destination (from), and route to create unique key
|
||||
// (ignoring gateway_id since multiple gateways can receive same route)
|
||||
const routeKey = `${originalSender}-${originalDestination}-${route.join('-')}`;
|
||||
if (activeTracerouteKeys.has(routeKey)) {
|
||||
return; // Already visualizing this route
|
||||
}
|
||||
|
||||
// Mark as active and set cooldown
|
||||
activeTracerouteKeys.add(routeKey);
|
||||
tracerouteCooldown[originalSender] = now;
|
||||
|
||||
// Build the complete path: to (original sender) -> route[0] -> route[1] -> ... -> from (original destination)
|
||||
const path = [originalSender]; // Start from original sender
|
||||
if (route.length > 0) {
|
||||
path.push(...route);
|
||||
}
|
||||
path.push(originalDestination); // End at original destination
|
||||
|
||||
// Visualize the traceroute with animated hops
|
||||
visualizeTraceroute(path, snrTowards, routeKey);
|
||||
}
|
||||
|
||||
function visualizeTraceroute(path, snrTowards, routeKey) {
|
||||
// Verify all nodes in path exist on map
|
||||
const pathMarkers = [];
|
||||
for (const nodeId of path) {
|
||||
const marker = findNodeMarkerById(nodeId);
|
||||
if (!marker) {
|
||||
// Node not on map, skip this traceroute
|
||||
activeTracerouteKeys.delete(routeKey);
|
||||
return;
|
||||
}
|
||||
pathMarkers.push(marker);
|
||||
}
|
||||
|
||||
// Store lines and overlays for this route key for cleanup
|
||||
const routeElements = {
|
||||
lines: [],
|
||||
startOverlay: null,
|
||||
endOverlay: null,
|
||||
};
|
||||
tracerouteLines[routeKey] = routeElements;
|
||||
|
||||
// Color starting node (first in path) green and destination node (last in path) red
|
||||
const startMarker = pathMarkers[0];
|
||||
const endMarker = pathMarkers[pathMarkers.length - 1];
|
||||
|
||||
const startOverlay = L.marker(startMarker.getLatLng(), {
|
||||
icon: iconTracerouteStart,
|
||||
zIndexOffset: 10000, // Ensure it's on top
|
||||
}).addTo(traceroutesLayerGroup);
|
||||
|
||||
const endOverlay = L.marker(endMarker.getLatLng(), {
|
||||
icon: iconTracerouteEnd,
|
||||
zIndexOffset: 10000, // Ensure it's on top
|
||||
}).addTo(traceroutesLayerGroup);
|
||||
|
||||
// Store overlays for cleanup
|
||||
routeElements.startOverlay = startOverlay;
|
||||
routeElements.endOverlay = endOverlay;
|
||||
|
||||
// Animate each hop sequentially
|
||||
let hopIndex = 0;
|
||||
const animateNextHop = () => {
|
||||
if (hopIndex >= pathMarkers.length - 1) {
|
||||
// All hops animated, cleanup after delay
|
||||
setTimeout(() => {
|
||||
if (tracerouteLines[routeKey]) {
|
||||
const routeElements = tracerouteLines[routeKey];
|
||||
// Remove all lines
|
||||
if (routeElements.lines) {
|
||||
routeElements.lines.forEach(line => {
|
||||
line.remove();
|
||||
});
|
||||
}
|
||||
// Remove node overlays
|
||||
if (routeElements.startOverlay) {
|
||||
routeElements.startOverlay.remove();
|
||||
}
|
||||
if (routeElements.endOverlay) {
|
||||
routeElements.endOverlay.remove();
|
||||
}
|
||||
delete tracerouteLines[routeKey];
|
||||
}
|
||||
activeTracerouteKeys.delete(routeKey);
|
||||
}, 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
const fromMarker = pathMarkers[hopIndex];
|
||||
const toMarker = pathMarkers[hopIndex + 1];
|
||||
const snr = hopIndex < snrTowards.length ? snrTowards[hopIndex] : null;
|
||||
|
||||
// Use orange color for all traceroute lines
|
||||
const lineColor = '#f97316'; // orange
|
||||
|
||||
// Create animated polyline for this hop with orange dotted style
|
||||
const line = L.polyline([fromMarker.getLatLng(), toMarker.getLatLng()], {
|
||||
color: lineColor,
|
||||
weight: 4,
|
||||
opacity: 0, // Start invisible
|
||||
// dashArray: '10, 5', // Dotted line style
|
||||
zIndexOffset: 10000,
|
||||
}).addTo(traceroutesLayerGroup);
|
||||
|
||||
// Fade in animation
|
||||
line.setStyle({ opacity: 1.0 });
|
||||
tracerouteLines[routeKey].lines.push(line);
|
||||
|
||||
// Animate next hop after 600ms delay
|
||||
hopIndex++;
|
||||
setTimeout(animateNextHop, 600);
|
||||
};
|
||||
|
||||
// Start animation
|
||||
animateNextHop();
|
||||
}
|
||||
|
||||
// Connect WebSocket when page loads
|
||||
connectWebSocket();
|
||||
|
||||
</script>
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue