diff --git a/meshview/templates/map.html b/meshview/templates/map.html
index e351b0a..8874412 100644
--- a/meshview/templates/map.html
+++ b/meshview/templates/map.html
@@ -27,6 +27,14 @@
.filter-checkbox {
margin: 0 10px;
}
+ .blinking-tooltip {
+ background: white;
+ color: black;
+ font-weight: bold;
+ border: 1px solid black;
+ border-radius: 4px;
+ padding: 2px 5px;
+ }
{% endblock %}
@@ -49,7 +57,8 @@
}).addTo(map);
var markers = {};
- var nodeIndex = {}; // node.id -> [lat, lng]
+ var markerById = {};
+ var nodeIndex = {};
var bounds = L.latLngBounds();
var channels = new Set();
@@ -66,11 +75,21 @@
last_update: {{ node.last_update | default("", true) | tojson }},
firmware: {{ (node.firmware or "") | tojson }},
id: {{ (node.node_id or "") | tojson }},
- isRouter: "{{ 'router' in node.role.lower() }}"
+ isRouter: {{ 'true' if 'router' in (node.role or '').lower() else 'false' }}
}{{ "," if not loop.last else "" }}
{% endfor %}
];
+ const portMap = {
+ 1: "Text",
+ 67: "Telemetry",
+ 3: "Position",
+ 70: "Traceroute",
+ 4: "Node Info",
+ 71: "Neighbour Info",
+ 73: "Map Report"
+ };
+
function timeAgo(date) {
var now = new Date();
var diff = now - new Date(date);
@@ -103,7 +122,7 @@
nodes.forEach(function(node) {
if (node.lat !== null && node.long !== null) {
let category = node.channel;
- let isRouter = node.isRouter === "True";
+ let isRouter = node.isRouter;
channels.add(category);
let color = hashToColor(category);
@@ -125,13 +144,14 @@
if (node.firmware) popupContent += `Firmware: ${node.firmware}
`;
var marker = L.circleMarker([node.lat, node.long], markerOptions).addTo(map);
- marker.on('click', function() {
- marker.bindPopup(popupContent).openPopup();
- setTimeout(() => {
- marker.closePopup();
- }, 2000); // 2000 ms = 3 seconds
- });
+ marker.nodeId = node.id;
+ markerById[node.id] = marker;
+ // show popup for 3 seconds
+ marker.on('click', function() {
+ marker.bindPopup(popupContent).openPopup();
+ setTimeout(() => marker.closePopup(), 3000);
+ });
if (!markers[category]) markers[category] = [];
markers[category].push({ marker, isRouter });
@@ -141,24 +161,22 @@
}
});
- // Fit to configured bounds
+ // Fit to bounds
var bayAreaBounds = [
[{{ site_config["site"]["map_top_left_lat"] }}, {{ site_config["site"]["map_top_left_lon"] }}],
[{{ site_config["site"]["map_bottom_right_lat"] }}, {{ site_config["site"]["map_bottom_right_lon"] }}]
];
map.fitBounds(bayAreaBounds);
- // Create channel filters
+ // Channel filters
let filterContainer = document.getElementById("filter-container");
channels.forEach(channel => {
let filterId = `filter-${channel.replace(/\s+/g, '-').toLowerCase()}`;
let color = hashToColor(channel);
- let filterHtml = `
-
- `;
- filterContainer.innerHTML += filterHtml;
+ let label = document.createElement('label');
+ label.style.color = color;
+ label.innerHTML = ` ${channel}`;
+ filterContainer.appendChild(label);
});
function updateMarkers() {
@@ -177,28 +195,23 @@
input.addEventListener("change", updateMarkers);
});
- // --- Lazy load edges on node click ---
- var edgeLayer = L.layerGroup().addTo(map); // initially empty
+ // --- Edges ---
+ var edgeLayer = L.layerGroup().addTo(map);
var edgesData = null;
function loadEdges(callback) {
- if (edgesData) {
- callback(edgesData);
- } else {
+ if (edgesData) callback(edgesData);
+ else {
fetch('/api/edges')
.then(res => res.json())
- .then(data => {
- edgesData = data.edges;
- callback(edgesData);
- })
+ .then(data => { edgesData = data.edges; callback(edgesData); })
.catch(err => console.error("Error loading edges:", err));
}
}
function onNodeClick(node) {
- loadEdges(function(edges) {
- edgeLayer.clearLayers(); // remove previous edges
-
+ loadEdges(edges => {
+ edgeLayer.clearLayers();
edges.forEach(edge => {
if (edge.from === node.id || edge.to === node.id) {
let from = nodeIndex[edge.from];
@@ -210,25 +223,107 @@
opacity: 1,
dashArray: edge.type === "traceroute" ? "5,5" : null
}).addTo(edgeLayer);
-
poly.bringToBack();
- poly.setStyle({opacity: 1}); // highlight edge immediately
+ poly.setStyle({opacity: 1});
}
}
});
});
}
- // Attach click events to nodes
nodes.forEach(node => {
if (node.lat !== null && node.long !== null) {
let marker = markers[node.channel].find(obj =>
obj.marker.getLatLng().lat === node.lat &&
obj.marker.getLatLng().lng === node.long
).marker;
-
marker.on('click', () => onNodeClick(node));
}
});
+
+
+ var lastFetchTime = null;
+
+ function fetchLatestPacket() {
+ fetch(`/api/packets?limit=1&since=1970-01-01T00:00:00Z`)
+ .then(res => res.json())
+ .then(data => {
+ if (data.packets && data.packets.length > 0) lastFetchTime = data.packets[0].import_time;
+ else lastFetchTime = new Date().toISOString();
+ console.log("Starting from:", lastFetchTime);
+ })
+ .catch(err => console.error("Error fetching latest packet:", err));
+ }
+
+ // Track active blink intervals per marker
+const activeBlinks = new Map();
+
+function blinkNode(marker, longName, portnum) {
+ // Clear any previous blinking interval
+ if (activeBlinks.has(marker)) {
+ clearInterval(activeBlinks.get(marker));
+ marker.setStyle({ fillColor: marker.originalColor });
+ if (marker.tooltip) map.removeLayer(marker.tooltip);
+ }
+
+ marker.originalColor = marker.options.fillColor;
+ let blinkCount = 0;
+ let portName = portMap[portnum] || `Port ${portnum}`;
+
+ // Create tooltip
+ let tooltip = L.tooltip({
+ permanent: true,
+ direction: 'top',
+ offset: [0, -marker.options.radius - 5],
+ className: 'blinking-tooltip'
+ }).setContent(`${longName} (${portName})`)
+ .setLatLng(marker.getLatLng());
+
+ tooltip.addTo(map);
+ marker.tooltip = tooltip;
+
+ let interval = setInterval(() => {
+ marker.setStyle({ fillColor: blinkCount % 2 === 0 ? 'yellow' : marker.originalColor });
+ marker.bringToFront();
+ blinkCount++;
+ if (blinkCount > 7) { // 3 seconds
+ clearInterval(interval);
+ marker.setStyle({ fillColor: marker.originalColor });
+ map.removeLayer(tooltip);
+ activeBlinks.delete(marker);
+ }
+ }, 500);
+
+ activeBlinks.set(marker, interval);
+}
+
+
+ function fetchNewPackets() {
+ if (!lastFetchTime) return;
+ fetch(`/api/packets?since=${lastFetchTime}`)
+ .then(res => res.json())
+ .then(data => {
+ if (!data.packets || data.packets.length === 0) return;
+
+ data.packets.forEach(packet => {
+ let marker = markerById[packet.from_node_id];
+ if (marker) {
+ let nodeData = nodes.find(n => n.id === packet.from_node_id);
+ if (nodeData) {
+ blinkNode(marker, nodeData.long_name, packet.portnum);
+ console.log(`Node: ${nodeData.short_name}, Port: ${portMap[packet.portnum] || packet.portnum}`);
+ }
+ }
+ });
+
+ let latestPacket = data.packets[data.packets.length - 1];
+ if (latestPacket && latestPacket.import_time) lastFetchTime = latestPacket.import_time;
+ })
+ .catch(err => console.error("Error fetching packets:", err));
+ }
+
+ fetchLatestPacket();
+ setInterval(fetchNewPackets, 1000);
+
{% endblock %}