From 8d42910fcadee21d3b41a16d2cf07b77da62fcb8 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 4 Mar 2026 23:10:35 +0000 Subject: [PATCH] improve link visibility and network-scoped viable pairs --- backend/src/db/index.ts | 17 +++++-- backend/src/ws/server.ts | 2 +- frontend/src/App.tsx | 1 + frontend/src/components/Map/MapView.tsx | 62 ++++++++++++++++++++----- 4 files changed, 65 insertions(+), 17 deletions(-) diff --git a/backend/src/db/index.ts b/backend/src/db/index.ts index f88acec..c0ea9aa 100644 --- a/backend/src/db/index.ts +++ b/backend/src/db/index.ts @@ -155,11 +155,20 @@ export async function getLastNPackets(n: number, network?: string) { export const MIN_LINK_OBSERVATIONS = 5; /** Returns only confirmed viable link pairs — compact for sending in initial WebSocket state. */ -export async function getViableLinkPairs(): Promise<[string, string][]> { +export async function getViableLinkPairs(network?: string): Promise<[string, string][]> { + const params: unknown[] = [MIN_LINK_OBSERVATIONS]; + const networkFilter = network ? 'AND a.network = $2 AND b.network = $2' : ''; + if (network) params.push(network); + const res = await pool.query<{ node_a_id: string; node_b_id: string }>( - `SELECT node_a_id, node_b_id FROM node_links - WHERE (itm_viable = true OR force_viable = true) AND observed_count >= $1`, - [MIN_LINK_OBSERVATIONS], + `SELECT nl.node_a_id, nl.node_b_id + FROM node_links nl + JOIN nodes a ON a.node_id = nl.node_a_id + JOIN nodes b ON b.node_id = nl.node_b_id + WHERE (nl.itm_viable = true OR nl.force_viable = true) + AND nl.observed_count >= $1 + ${networkFilter}`, + params, ); return res.rows.map((r) => [r.node_a_id, r.node_b_id]); } diff --git a/backend/src/ws/server.ts b/backend/src/ws/server.ts index c4873b3..1218892 100644 --- a/backend/src/ws/server.ts +++ b/backend/src/ws/server.ts @@ -54,7 +54,7 @@ export function initWebSocketServer(httpServer: Server): WebSocketServer { // Send initial state: known nodes + last 5 minutes of packets try { const [nodes, packets, viablePairs] = await Promise.all([ - getNodes(network), getLastNPackets(10, network), getViableLinkPairs(), + getNodes(network), getLastNPackets(10, network), getViableLinkPairs(network), ]); const initMsg: WSMessage = { type: 'initial_state', diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4d31e91..3e2b8e9 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -137,6 +137,7 @@ export const App: React.FC = () => { showClientNodes={filters.clientNodes} showLinks={filters.links} viablePairsArr={viablePairsArr} + linkMetrics={linkMetrics} packetPath={packetPath} betaPath={betaPacketPath} showBetaPaths={filters.betaPaths || pinnedPacketId !== null} diff --git a/frontend/src/components/Map/MapView.tsx b/frontend/src/components/Map/MapView.tsx index aa7f926..af21462 100644 --- a/frontend/src/components/Map/MapView.tsx +++ b/frontend/src/components/Map/MapView.tsx @@ -3,6 +3,7 @@ import { MapContainer, TileLayer, useMap, Pane, Polygon, Polyline } from 'react- import type { LatLngExpression, Map as LeafletMap, Polyline as LeafletPolyline } from 'leaflet'; import type { MeshNode, PacketArc } from '../../hooks/useNodes.js'; import type { NodeCoverage } from '../../hooks/useCoverage.js'; +import type { LinkMetrics } from '../../utils/pathing.js'; import { NodeMarker } from './NodeMarker.js'; import { PacketArcLayer } from './PacketArcLayer.js'; import { NodeSearch } from './NodeSearch.js'; @@ -88,6 +89,7 @@ interface MapViewProps { showClientNodes: boolean; showLinks: boolean; viablePairsArr: [string, string][]; + linkMetrics: Map; packetPath: [number, number][] | null; betaPath: [number, number][] | null; showBetaPaths: boolean; @@ -101,7 +103,7 @@ const DEFAULT_ZOOM = 11; export const MapView: React.FC = ({ nodes, arcs, activeNodes, coverage, showPackets, showCoverage, showClientNodes, - showLinks, viablePairsArr, packetPath, betaPath, showBetaPaths, pathOpacity, onMapReady, + showLinks, viablePairsArr, linkMetrics, packetPath, betaPath, showBetaPaths, pathOpacity, onMapReady, }) => { const [map, setMap] = useState(null); @@ -181,19 +183,49 @@ export const MapView: React.FC = ({ return m; }, [coverage]); + const linkKey = (a: string, b: string) => (a < b ? `${a}:${b}` : `${b}:${a}`); + + const linkColor = (pathLossDb: number | null | undefined) => { + if (pathLossDb == null) return '#fbbf24'; + if (pathLossDb <= 120) return '#22c55e'; + if (pathLossDb <= 135) return '#fbbf24'; + return '#ef4444'; + }; + // Resolve viable link pairs to lat/lon polyline positions const linkLines = useMemo(() => { if (!showLinks || viablePairsArr.length === 0) return []; - const lines: [number, number][][] = []; + const lines: Array<{ + key: string; + positions: [number, number][]; + observedCount: number; + pathLossDb: number | null | undefined; + }> = []; for (const [aId, bId] of viablePairsArr) { const a = nodes.get(aId); const b = nodes.get(bId); - if (hasCoords(a) && hasCoords(b)) { - lines.push([[a.lat, a.lon], [b.lat, b.lon]]); + if ( + hasCoords(a) + && hasCoords(b) + && (Date.now() - new Date(a.last_seen).getTime()) < FOURTEEN_DAYS_MS + && (Date.now() - new Date(b.last_seen).getTime()) < FOURTEEN_DAYS_MS + && !a.name?.includes('🚫') + && !b.name?.includes('🚫') + && (a.role === undefined || a.role === 2) + && (b.role === undefined || b.role === 2) + ) { + const key = linkKey(aId, bId); + const metrics = linkMetrics.get(key); + lines.push({ + key, + positions: [[a.lat, a.lon], [b.lat, b.lon]], + observedCount: metrics?.observed_count ?? 0, + pathLossDb: metrics?.itm_path_loss_db, + }); } } return lines; - }, [showLinks, viablePairsArr, nodes]); + }, [showLinks, viablePairsArr, nodes, linkMetrics]); return (
@@ -240,18 +272,24 @@ export const MapView: React.FC = ({ {/* Confirmed link lines — ITM-viable node pairs */} {showLinks && linkLines.length > 0 && ( - {linkLines.map((positions, i) => ( + {linkLines.map((line) => { + const obs = Math.max(1, line.observedCount); + const strength = Math.log10(obs + 1); + const opacity = Math.min(0.85, 0.35 + strength * 0.22); + const weight = Math.min(3.2, 1.0 + strength * 1.1); + return ( - ))} + ); + })} )}