Add connections endpoint and UI configuration for connections time period and colored lines

This commit is contained in:
Anton Roslund 2026-01-07 20:32:18 +01:00
parent 1333447398
commit 556dde517b
2 changed files with 407 additions and 3 deletions

View file

@ -155,6 +155,14 @@ app.get('/api', async (req, res) => {
"time_to": "Only include traceroutes updated before this unix timestamp (milliseconds)"
}
},
{
"path": "/api/v1/connections",
"description": "Aggregated edges between nodes from traceroutes",
"params": {
"time_from": "Only include edges created after this unix timestamp (milliseconds)",
"time_to": "Only include edges created before this unix timestamp (milliseconds)"
}
},
{
"path": "/api/v1/nodes/:nodeId/position-history",
"description": "Position history for a meshtastic node",
@ -698,6 +706,186 @@ app.get('/api/v1/traceroutes', async (req, res) => {
}
});
// Aggregated edges endpoint
// GET /api/v1/connections?time_from=...&time_to=...
app.get('/api/v1/connections', async (req, res) => {
try {
const timeFrom = req.query.time_from ? parseInt(req.query.time_from) : undefined;
const timeTo = req.query.time_to ? parseInt(req.query.time_to) : undefined;
// Query edges from database
const edges = await prisma.edge.findMany({
where: {
created_at: {
gte: timeFrom ? new Date(timeFrom) : undefined,
lte: timeTo ? new Date(timeTo) : undefined,
},
// Only include edges where both nodes have positions
from_latitude: { not: null },
from_longitude: { not: null },
to_latitude: { not: null },
to_longitude: { not: null },
},
orderBy: [
{ created_at: 'desc' },
{ packet_id: 'desc' },
],
});
// Collect all unique node IDs from edges
const nodeIds = new Set();
for (const edge of edges) {
nodeIds.add(edge.from_node_id);
nodeIds.add(edge.to_node_id);
}
// Fetch current positions for all nodes
const nodes = await prisma.node.findMany({
where: {
node_id: { in: Array.from(nodeIds) },
},
select: {
node_id: true,
latitude: true,
longitude: true,
},
});
// Create a map of current node positions
const nodePositions = new Map();
for (const node of nodes) {
nodePositions.set(node.node_id, {
latitude: node.latitude,
longitude: node.longitude,
});
}
// Filter edges: only include edges where both nodes are still at the same location
const validEdges = edges.filter(edge => {
const fromCurrentPos = nodePositions.get(edge.from_node_id);
const toCurrentPos = nodePositions.get(edge.to_node_id);
// Skip if either node doesn't exist or doesn't have a current position
if (!fromCurrentPos || !toCurrentPos ||
fromCurrentPos.latitude === null || fromCurrentPos.longitude === null ||
toCurrentPos.latitude === null || toCurrentPos.longitude === null) {
return false;
}
// Check if stored positions match current positions
const fromMatches = fromCurrentPos.latitude === edge.from_latitude &&
fromCurrentPos.longitude === edge.from_longitude;
const toMatches = toCurrentPos.latitude === edge.to_latitude &&
toCurrentPos.longitude === edge.to_longitude;
return fromMatches && toMatches;
});
// Normalize node pairs: always use min/max to treat A->B and B->A as same connection
const connectionsMap = new Map();
for (const edge of validEdges) {
const nodeA = edge.from_node_id < edge.to_node_id ? edge.from_node_id : edge.to_node_id;
const nodeB = edge.from_node_id < edge.to_node_id ? edge.to_node_id : edge.from_node_id;
const key = `${nodeA}-${nodeB}`;
if (!connectionsMap.has(key)) {
connectionsMap.set(key, {
node_a: nodeA,
node_b: nodeB,
direction_ab: [], // A -> B edges
direction_ba: [], // B -> A edges
});
}
const connection = connectionsMap.get(key);
const isAB = edge.from_node_id === nodeA;
// Add edge to appropriate direction
if (isAB) {
connection.direction_ab.push({
snr: edge.snr,
snr_db: edge.snr / 4, // Convert to dB
created_at: edge.created_at,
packet_id: edge.packet_id,
source: edge.source,
});
} else {
connection.direction_ba.push({
snr: edge.snr,
snr_db: edge.snr / 4,
created_at: edge.created_at,
packet_id: edge.packet_id,
source: edge.source,
});
}
}
// Aggregate each connection
const connections = Array.from(connectionsMap.values()).map(conn => {
// Deduplicate edges by packet_id for each direction (keep first occurrence, which is most recent)
const dedupeByPacketId = (edges) => {
const seen = new Set();
return edges.filter(edge => {
if (seen.has(edge.packet_id)) {
return false;
}
seen.add(edge.packet_id);
return true;
});
};
const deduplicatedAB = dedupeByPacketId(conn.direction_ab);
const deduplicatedBA = dedupeByPacketId(conn.direction_ba);
// Calculate average SNR for A->B (using deduplicated edges)
const avgSnrAB = deduplicatedAB.length > 0
? deduplicatedAB.reduce((sum, e) => sum + e.snr_db, 0) / deduplicatedAB.length
: null;
// Calculate average SNR for B->A (using deduplicated edges)
const avgSnrBA = deduplicatedBA.length > 0
? deduplicatedBA.reduce((sum, e) => sum + e.snr_db, 0) / deduplicatedBA.length
: null;
// Get last 5 edges for each direction (already sorted by created_at DESC, packet_id DESC, now deduplicated)
const last5AB = deduplicatedAB.slice(0, 5);
const last5BA = deduplicatedBA.slice(0, 5);
// Determine worst average SNR
const worstAvgSnrDb = [avgSnrAB, avgSnrBA]
.filter(v => v !== null)
.reduce((min, val) => val < min ? val : min, Infinity);
return {
node_a: conn.node_a,
node_b: conn.node_b,
direction_ab: {
avg_snr_db: avgSnrAB,
last_5_edges: last5AB,
total_count: deduplicatedAB.length, // Use deduplicated count
},
direction_ba: {
avg_snr_db: avgSnrBA,
last_5_edges: last5BA,
total_count: deduplicatedBA.length, // Use deduplicated count
},
worst_avg_snr_db: worstAvgSnrDb !== Infinity ? worstAvgSnrDb : null,
};
}).filter(conn => conn.worst_avg_snr_db !== null); // Only return connections with at least one direction
res.json({
connections: connections,
});
} catch (err) {
console.error(err);
res.status(500).json({
message: "Something went wrong, try again later.",
});
}
});
app.get('/api/v1/nodes/:nodeId/position-history', async (req, res) => {
try {