Compare commits

...

7 Commits

2 changed files with 176 additions and 83 deletions

View File

@@ -1096,7 +1096,7 @@ client.on("message", async (topic, message) => {
// Extract edges from neighbour info
try {
const fromNodeId = envelope.packet.from;
const toNodeId = envelope.packet.from;
const neighbors = neighbourInfo.neighbors || [];
const packetId = envelope.packet.id;
const channelId = envelope.channelId;
@@ -1115,7 +1115,7 @@ client.on("message", async (topic, message) => {
continue;
}
const toNodeId = neighbour.nodeId;
const fromNodeId = neighbour.nodeId;
const snr = neighbour.snr;
// Fetch node positions from Node table

View File

@@ -1303,29 +1303,6 @@
</select>
</div>
<!-- configNodesDisconnectedAgeInSeconds -->
<div class="p-2">
<label class="block text-sm font-medium text-gray-900">Nodes Disconnected Age</label>
<div class="text-xs text-gray-600 mb-2">Nodes that have not uplinked to MQTT in this time will show as blue icons. Reload to update map.</div>
<select v-model="configNodesDisconnectedAgeInSeconds" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
<option value="900">15 minutes</option>
<option value="1800">30 minutes</option>
<option value="2700">45 minutes</option>
<option value="3600">1 hour</option>
<option value="7200">2 hours</option>
<option value="10800">3 hours</option>
<option value="21600">6 hours</option>
<option value="43200">12 hours</option>
<option value="86400">24 hours</option>
<option value="172800">2 days</option>
<option value="259200">3 days</option>
<option value="345600">4 days</option>
<option value="432000">5 days</option>
<option value="518400">6 days</option>
<option value="604800">7 days</option>
</select>
</div>
<!-- configNodesOfflineAgeInSeconds -->
<div class="p-2">
<label class="block text-sm font-medium text-gray-900">Nodes Offline Age</label>
@@ -1374,8 +1351,8 @@
<!-- configConnectionsTimePeriodInSeconds -->
<div class="p-2">
<label class="block text-sm font-medium text-gray-900">Connections Time Period</label>
<div class="text-xs text-gray-600 mb-2">Edges within this time period are shown in the Connections layer. Reload to update map.</div>
<label class="block text-sm font-medium text-gray-900">Connections Max Age</label>
<div class="text-xs text-gray-600 mb-2">Edges from traceroutes and neighbour info within this time period are shown in the Connections layer. Reload to update map.</div>
<select v-model="configConnectionsTimePeriodInSeconds" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
<option value="300">5 minutes</option>
<option value="900">15 minutes</option>
@@ -1402,6 +1379,31 @@
<div class="text-xs text-gray-600">Colors the connection lines by the average SNR in the worst direction. Reload to update map.</div>
</div>
<!-- configConnectionsBidirectionalOnly -->
<div class="p-2">
<div class="flex items-start">
<div class="flex items-center h-5">
<input type="checkbox" v-model="configConnectionsBidirectionalOnly" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300" required>
</div>
<label class="ml-2 text-sm font-medium text-gray-900">Bidirectional Connections Only</label>
</div>
<div class="text-xs text-gray-600">Only show connections where data flows in both directions. Reload to update map.</div>
</div>
<!-- configConnectionsMinSnrDb -->
<div class="p-2">
<label class="block text-sm font-medium text-gray-900">Connections Minimum SNR (dB)</label>
<div class="text-xs text-gray-600 mb-2">Only show connections where at least one direction has SNR above this threshold. Leave empty to show all connections. Reload to update map.</div>
<input type="number" v-model="configConnectionsMinSnrDb" placeholder="e.g. -10" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
<div class="mt-2 flex items-start">
<div class="flex items-center h-5">
<input type="checkbox" v-model="configConnectionsBidirectionalMinSnr" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300" required>
</div>
<label class="ml-2 text-sm font-medium text-gray-900">Bidirectional Minimum SNR</label>
</div>
<div class="text-xs text-gray-600 ml-6">If checked, all existing directions must meet the minimum SNR threshold (both directions if bidirectional, single direction if unidirectional).</div>
</div>
<!-- configConnectionsMaxDistanceInMeters -->
<div class="p-2">
<label class="block text-sm font-medium text-gray-900">Connections Max Distance (meters)</label>
@@ -1657,20 +1659,6 @@
}
}
function getConfigNodesDisconnectedAgeInSeconds() {
// default to showing nodes as recently uplinked if heard in the last 30 minutes
const value = localStorage.getItem("config_nodes_disconnected_age_in_seconds");
return value != null ? parseInt(value) : 1800;
}
function setConfigNodesDisconnectedAgeInSeconds(value) {
if(value != null){
return localStorage.setItem("config_nodes_disconnected_age_in_seconds", value);
} else {
return localStorage.removeItem("config_nodes_disconnected_age_in_seconds");
}
}
function getConfigNodesOfflineAgeInSeconds() {
const value = localStorage.getItem("config_nodes_offline_age_in_seconds");
return value != null ? parseInt(value) : null;
@@ -1719,8 +1707,8 @@
function getConfigConnectionsTimePeriodInSeconds() {
const value = localStorage.getItem("config_connections_time_period_in_seconds");
// default to 24 hours if unset
return value != null ? parseInt(value) : 86400;
// default to 7 days if unset
return value != null ? parseInt(value) : 604800;
}
function setConfigConnectionsTimePeriodInSeconds(value) {
@@ -1740,6 +1728,51 @@
return localStorage.setItem("config_connections_colored_lines", value);
}
function getConfigConnectionsBidirectionalOnly() {
const value = localStorage.getItem("config_connections_bidirectional_only");
// disable bidirectional filter by default
if(value === null){
return false;
}
return value === "true";
}
function setConfigConnectionsBidirectionalOnly(value) {
return localStorage.setItem("config_connections_bidirectional_only", value);
}
function getConfigConnectionsMinSnrDb() {
const value = localStorage.getItem("config_connections_min_snr_db");
// default to null (unset)
if(value === null || value === ""){
return null;
}
const parsed = parseFloat(value);
return isNaN(parsed) ? null : parsed;
}
function setConfigConnectionsMinSnrDb(value) {
if(value === null || value === "" || value === undefined){
return localStorage.removeItem("config_connections_min_snr_db");
}
// Convert to string for localStorage (handles both number and string inputs)
const stringValue = typeof value === "number" ? value.toString() : String(value);
return localStorage.setItem("config_connections_min_snr_db", stringValue);
}
function getConfigConnectionsBidirectionalMinSnr() {
const value = localStorage.getItem("config_connections_bidirectional_min_snr");
// disable bidirectional minimum SNR by default
if(value === null){
return false;
}
return value === "true";
}
function setConfigConnectionsBidirectionalMinSnr(value) {
return localStorage.setItem("config_connections_bidirectional_min_snr", value);
}
function isMobile() {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}
@@ -1754,7 +1787,6 @@
isShowingAnnouncement: this.shouldShowAnnouncement(),
configNodesMaxAgeInSeconds: window.getConfigNodesMaxAgeInSeconds(),
configNodesDisconnectedAgeInSeconds: window.getConfigNodesDisconnectedAgeInSeconds(),
configNodesOfflineAgeInSeconds: window.getConfigNodesOfflineAgeInSeconds(),
configWaypointsMaxAgeInSeconds: window.getConfigWaypointsMaxAgeInSeconds(),
configConnectionsMaxDistanceInMeters: window.getConfigConnectionsMaxDistanceInMeters(),
@@ -1764,6 +1796,9 @@
configTemperatureFormat: window.getConfigTemperatureFormat(),
configConnectionsTimePeriodInSeconds: window.getConfigConnectionsTimePeriodInSeconds(),
configConnectionsColoredLines: window.getConfigConnectionsColoredLines(),
configConnectionsBidirectionalOnly: window.getConfigConnectionsBidirectionalOnly(),
configConnectionsMinSnrDb: window.getConfigConnectionsMinSnrDb(),
configConnectionsBidirectionalMinSnr: window.getConfigConnectionsBidirectionalMinSnr(),
isShowingHardwareModels: false,
hardwareModelStats: null,
@@ -2654,9 +2689,6 @@
configNodesMaxAgeInSeconds() {
window.setConfigNodesMaxAgeInSeconds(this.configNodesMaxAgeInSeconds);
},
configNodesDisconnectedAgeInSeconds() {
window.setConfigNodesDisconnectedAgeInSeconds(this.configNodesDisconnectedAgeInSeconds);
},
configNodesOfflineAgeInSeconds() {
window.setConfigNodesOfflineAgeInSeconds(this.configNodesOfflineAgeInSeconds);
},
@@ -2684,6 +2716,15 @@
configConnectionsColoredLines() {
window.setConfigConnectionsColoredLines(this.configConnectionsColoredLines);
},
configConnectionsBidirectionalOnly() {
window.setConfigConnectionsBidirectionalOnly(this.configConnectionsBidirectionalOnly);
},
configConnectionsMinSnrDb() {
window.setConfigConnectionsMinSnrDb(this.configConnectionsMinSnrDb);
},
configConnectionsBidirectionalMinSnr() {
window.setConfigConnectionsBidirectionalMinSnr(this.configConnectionsBidirectionalMinSnr);
},
deviceMetricsTimeRange() {
this.loadNodeDeviceMetrics(this.selectedNode.node_id);
},
@@ -2867,7 +2908,7 @@
},
"Overlays": {
"Legend": legendLayerGroup,
"Backbone Connection": backboneConnectionsLayerGroup,
"Backbone Connections": backboneConnectionsLayerGroup,
"Connections": connectionsLayerGroup,
"Waypoints": waypointsLayerGroup,
"Position History": nodePositionHistoryLayerGroup,
@@ -3255,6 +3296,48 @@
if (!otherNode || !otherNodeMarker) continue;
// Apply bidirectional filter
const configConnectionsBidirectionalOnly = getConfigConnectionsBidirectionalOnly();
if(configConnectionsBidirectionalOnly){
const hasDirectionAB = connection.direction_ab && connection.direction_ab.avg_snr_db != null;
const hasDirectionBA = connection.direction_ba && connection.direction_ba.avg_snr_db != null;
if(!hasDirectionAB || !hasDirectionBA){
continue;
}
}
// Apply minimum SNR filter
const configConnectionsMinSnrDb = getConfigConnectionsMinSnrDb();
if(configConnectionsMinSnrDb != null){
const snrAB = connection.direction_ab && connection.direction_ab.avg_snr_db != null ? connection.direction_ab.avg_snr_db : null;
const snrBA = connection.direction_ba && connection.direction_ba.avg_snr_db != null ? connection.direction_ba.avg_snr_db : null;
const configConnectionsBidirectionalMinSnr = getConfigConnectionsBidirectionalMinSnr();
let hasSnrAboveThreshold;
if(configConnectionsBidirectionalMinSnr){
// Bidirectional mode: ALL existing directions must meet threshold
const directionsToCheck = [];
if(snrAB != null) directionsToCheck.push(snrAB);
if(snrBA != null) directionsToCheck.push(snrBA);
if(directionsToCheck.length === 0){
// No SNR data in either direction, skip
hasSnrAboveThreshold = false;
} else {
// All existing directions must be above threshold
hasSnrAboveThreshold = directionsToCheck.every(snr => snr > configConnectionsMinSnrDb);
}
} else {
// Default mode: EITHER direction has SNR above threshold
hasSnrAboveThreshold = (snrAB != null && snrAB > configConnectionsMinSnrDb) || (snrBA != null && snrBA > configConnectionsMinSnrDb);
}
if(!hasSnrAboveThreshold){
continue;
}
}
// Calculate distance
const distanceInMeters = nodeMarker.getLatLng().distanceTo(otherNodeMarker.getLatLng()).toFixed(2);
const configConnectionsMaxDistanceInMeters = getConfigConnectionsMaxDistanceInMeters();
@@ -3340,13 +3423,6 @@
return string.replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
// determine if node was recently heard uplinking packets to mqtt
function hasNodeUplinkedToMqttRecently(node) {
const now = moment();
const configNodesDisconnectedAgeInSeconds = getConfigNodesDisconnectedAgeInSeconds();
const millisecondsSinceNodeLastUplinkedToMqtt = now.diff(moment(node.mqtt_connection_state_updated_at));
return millisecondsSinceNodeLastUplinkedToMqtt < configNodesDisconnectedAgeInSeconds * 1000;
}
function onNodesUpdated(updatedNodes) {
@@ -3416,12 +3492,6 @@
zIndexOffset = -1000;
}
// determine if node was recently heard uplinking packets to mqtt
//const nodeHasUplinkedToMqttRecently = hasNodeUplinkedToMqttRecently(node);
//if(nodeHasUplinkedToMqttRecently){
// icon = iconMqttConnected;
//}
// To not have overlapping nodes.
var latJitter = 0;
var lonJitter = 0;
@@ -3665,6 +3735,48 @@
continue;
}
// Apply bidirectional filter
const configConnectionsBidirectionalOnly = getConfigConnectionsBidirectionalOnly();
if(configConnectionsBidirectionalOnly){
const hasDirectionAB = connection.direction_ab && connection.direction_ab.avg_snr_db != null;
const hasDirectionBA = connection.direction_ba && connection.direction_ba.avg_snr_db != null;
if(!hasDirectionAB || !hasDirectionBA){
continue;
}
}
// Apply minimum SNR filter
const configConnectionsMinSnrDb = getConfigConnectionsMinSnrDb();
if(configConnectionsMinSnrDb != null){
const snrAB = connection.direction_ab && connection.direction_ab.avg_snr_db != null ? connection.direction_ab.avg_snr_db : null;
const snrBA = connection.direction_ba && connection.direction_ba.avg_snr_db != null ? connection.direction_ba.avg_snr_db : null;
const configConnectionsBidirectionalMinSnr = getConfigConnectionsBidirectionalMinSnr();
let hasSnrAboveThreshold;
if(configConnectionsBidirectionalMinSnr){
// Bidirectional mode: ALL existing directions must meet threshold
const directionsToCheck = [];
if(snrAB != null) directionsToCheck.push(snrAB);
if(snrBA != null) directionsToCheck.push(snrBA);
if(directionsToCheck.length === 0){
// No SNR data in either direction, skip
hasSnrAboveThreshold = false;
} else {
// All existing directions must be above threshold
hasSnrAboveThreshold = directionsToCheck.every(snr => snr > configConnectionsMinSnrDb);
}
} else {
// Default mode: EITHER direction has SNR above threshold
hasSnrAboveThreshold = (snrAB != null && snrAB > configConnectionsMinSnrDb) || (snrBA != null && snrBA > configConnectionsMinSnrDb);
}
if(!hasSnrAboveThreshold){
continue;
}
}
// Calculate distance between nodes
const distanceInMeters = nodeAMarker.getLatLng().distanceTo(nodeBMarker.getLatLng()).toFixed(2);
@@ -3710,7 +3822,7 @@
event.target.closeTooltip();
});
// If both nodes are backbone nodes, also add to backbone layer group with arrows
// If both nodes are backbone nodes, also add to backbone layer group
if (nodeA.is_backbone && nodeB.is_backbone) {
const backboneLine = L.polyline([
nodeAMarker.getLatLng(),
@@ -3719,13 +3831,6 @@
color: lineColor,
opacity: 0.75,
weight: 3,
}).arrowheads({
size: '10px',
fill: true,
offsets: {
start: '25px',
end: '25px',
},
}).addTo(backboneConnectionsLayerGroup);
backboneLine.bindTooltip(tooltip, {
@@ -3784,7 +3889,7 @@
}
tooltip += `<br/>[${escapeString(node.short_name)}] ${escapeString(node.long_name)}`;
tooltip += `<br/>${positionHistory.latitude}, ${positionHistory.longitude}`;
tooltip += `<br/>Heard on: ${moment(new Date(positionHistory.created_at)).format("DD/MM/YYYY hh:mm A")}`;
tooltip += `<br/>Heard on: ${moment(new Date(positionHistory.created_at)).format("YYYY-MM-DD HH:mm")}`;
// add gateway info if available
if(positionHistory.gateway_id){
@@ -3967,24 +4072,11 @@
function getTooltipContentForNode(node) {
// determine if node was recently heard uplinking packets to mqtt
const nodeHasUplinkedToMqttRecently = hasNodeUplinkedToMqttRecently(node);
var mqttStatus = `<span class="text-blue-700">Disconnected</span>`;
if(node.mqtt_connection_state_updated_at){
var mqttStatusUpdatedAt = moment(new Date(node.mqtt_connection_state_updated_at)).fromNow();
if(nodeHasUplinkedToMqttRecently){
mqttStatus = `<span><span class="text-green-700">Connected</span> (${mqttStatusUpdatedAt})</span>`;
} else {
mqttStatus = `<span><span class="text-blue-700">Disconnected</span> (${mqttStatusUpdatedAt})</span>`;
}
}
var loraFrequencyRange = getRegionFrequencyRange(node.region_name);
var tooltip = `<img class="mb-4 w-40 mx-auto" src="/images/devices/${node.hardware_model_name}.png" onerror="this.classList.add('hidden')"/>` +
`<b>${escapeString(node.long_name)}</b>` +
`<br/>Short Name: ${escapeString(node.short_name)}` +
`<br/>MQTT: ${mqttStatus}` +
(node.num_online_local_nodes != null ? `<br/>Local Nodes Online: ${node.num_online_local_nodes}` : '') +
(node.position_precision != null && node.position_precision !== 32 ? `<br/>Position Precision: ${formatPositionPrecision(node.position_precision)}` : '') +
`<br/><br/>Role: ${node.role_name}` +
@@ -4021,6 +4113,7 @@
tooltip += `<br/><br/>ID: ${node.node_id}`;
tooltip += `<br/>Hex ID: ${node.node_id_hex}`;
tooltip += `<br/>Updated: ${moment(new Date(node.updated_at)).fromNow()}`;
tooltip += (node.mqtt_connection_state_updated_at ? `<br/>MQTT Updated: ${moment(new Date(node.mqtt_connection_state_updated_at)).fromNow()}` : '');
tooltip += (node.neighbours_updated_at ? `<br/>Neighbours Updated: ${moment(new Date(node.neighbours_updated_at)).fromNow()}` : '');
tooltip += (node.position_updated_at ? `<br/>Position Updated: ${moment(new Date(node.position_updated_at)).fromNow()}` : '');