path magic

This commit is contained in:
ajvpot
2025-09-17 17:46:43 +02:00
parent ea32ccea77
commit fe6e44cc80
5 changed files with 252 additions and 12 deletions

View File

@@ -10,12 +10,13 @@ export async function GET(req: Request) {
const maxLng = searchParams.get("maxLng");
const nodeTypes = searchParams.getAll("nodeTypes");
const lastSeen = searchParams.get("lastSeen");
const region = searchParams.get("region");
const includeNeighbors = searchParams.get("includeNeighbors") === "true";
const positions = await getNodePositions({ minLat, maxLat, minLng, maxLng, nodeTypes, lastSeen });
if (includeNeighbors) {
const neighbors = await getAllNodeNeighbors(lastSeen, minLat, maxLat, minLng, maxLng, nodeTypes);
const neighbors = await getAllNodeNeighbors(lastSeen, minLat, maxLat, minLng, maxLng, nodeTypes, region || undefined);
return NextResponse.json({
nodes: positions,
neighbors: neighbors

View File

@@ -10,8 +10,9 @@ export async function GET(req: Request) {
const maxLng = searchParams.get("maxLng");
const nodeTypes = searchParams.getAll("nodeTypes");
const lastSeen = searchParams.get("lastSeen");
const region = searchParams.get("region");
const neighbors = await getAllNodeNeighbors(lastSeen, minLat, maxLat, minLng, maxLng, nodeTypes);
const neighbors = await getAllNodeNeighbors(lastSeen, minLat, maxLat, minLng, maxLng, nodeTypes, region || undefined);
return NextResponse.json(neighbors);
} catch (error) {

View File

@@ -372,6 +372,42 @@ function AllNeighborLines({
visibleNodeIds.has(connection.source_node) && visibleNodeIds.has(connection.target_node)
);
// Calculate logarithmic thresholds based on packet counts for path connections
const pathConnections = visibleConnections.filter(conn => conn.connection_type === 'path');
const packetCounts = pathConnections.map(conn => conn.packet_count).sort((a, b) => a - b);
const getLogThresholds = (counts: number[]) => {
if (counts.length === 0) return { min: 1, t1: 1, t2: 1, t3: 1, t4: 1, max: 1 };
const min = Math.max(1, counts[0]); // Ensure minimum is at least 1 for log calculation
const max = counts[counts.length - 1];
if (min === max) {
return { min, t1: min, t2: min, t3: min, t4: min, max };
}
// Use logarithmic scale to create thresholds
const logMin = Math.log10(min);
const logMax = Math.log10(max);
const logRange = logMax - logMin;
const t1 = Math.pow(10, logMin + logRange * 0.2);
const t2 = Math.pow(10, logMin + logRange * 0.4);
const t3 = Math.pow(10, logMin + logRange * 0.6);
const t4 = Math.pow(10, logMin + logRange * 0.8);
return {
min,
t1: Math.round(t1),
t2: Math.round(t2),
t3: Math.round(t3),
t4: Math.round(t4),
max
};
};
const thresholds = getLogThresholds(packetCounts);
return (
<>
{visibleConnections.map((connection) => {
@@ -380,14 +416,34 @@ function AllNeighborLines({
[connection.target_latitude, connection.target_longitude]
];
// Different colors based on connection type and logarithmic packet count
const getConnectionColor = (connectionType: string, packetCount: number) => {
if (connectionType === 'direct') {
return '#8b5cf6'; // Purple for direct connections
}
// For path connections, use logarithmic thresholds for color intensity
if (packetCount >= thresholds.t4) return '#dc2626'; // Red for highest log range
if (packetCount >= thresholds.t3) return '#ea580c'; // Dark orange
if (packetCount >= thresholds.t2) return '#f59e0b'; // Orange
if (packetCount >= thresholds.t1) return '#eab308'; // Yellow
if (packetCount > thresholds.min) return '#84cc16'; // Light green for above minimum
return '#6b7280'; // Gray for minimum traffic
};
const lineColor = getConnectionColor(connection.connection_type, connection.packet_count);
// Consistent line weight for all connections
const lineWeight = connection.connection_type === 'direct' ? 2 : 1;
return (
<Polyline
key={`${connection.source_node}-${connection.target_node}`}
key={`${connection.source_node}-${connection.target_node}-${connection.connection_type}`}
positions={positions}
pathOptions={{
color: '#8b5cf6', // Purple color for all-neighbors view
weight: 1,
opacity: 0.5,
color: lineColor,
weight: lineWeight,
opacity: 0.7,
}}
/>
);
@@ -490,6 +546,9 @@ export default function MapView({ target = '_self' }: MapViewProps = {}) {
if (config?.lastSeen !== null && config?.lastSeen !== undefined) {
params.push(`lastSeen=${config.lastSeen}`);
}
if (config?.selectedRegion) {
params.push(`region=${encodeURIComponent(config.selectedRegion)}`);
}
if (includeNeighbors) {
params.push('includeNeighbors=true');
}
@@ -656,7 +715,7 @@ export default function MapView({ target = '_self' }: MapViewProps = {}) {
return () => {
fetchController.current?.abort();
};
}, [bounds, config?.nodeTypes, config?.lastSeen, fetchNodes, showAllNeighbors]);
}, [bounds, config?.nodeTypes, config?.lastSeen, config?.selectedRegion, fetchNodes, showAllNeighbors]);
return (
<div style={{ width: "100%", height: "100%", position: "relative" }}>
@@ -736,6 +795,83 @@ export default function MapView({ target = '_self' }: MapViewProps = {}) {
/>
)}
</MapContainer>
{/* Traffic Legend */}
{showAllNeighbors && allNeighborConnections.length > 0 && (() => {
// Calculate logarithmic thresholds for legend display
const pathConnections = allNeighborConnections.filter(conn => conn.connection_type === 'path');
const packetCounts = pathConnections.map(conn => conn.packet_count).sort((a, b) => a - b);
const legendThresholds = packetCounts.length > 0 ? (() => {
const min = Math.max(1, packetCounts[0]);
const max = packetCounts[packetCounts.length - 1];
if (min === max) {
return { min, t1: min, t2: min, t3: min, t4: min, max };
}
const logMin = Math.log10(min);
const logMax = Math.log10(max);
const logRange = logMax - logMin;
return {
min,
t1: Math.round(Math.pow(10, logMin + logRange * 0.2)),
t2: Math.round(Math.pow(10, logMin + logRange * 0.4)),
t3: Math.round(Math.pow(10, logMin + logRange * 0.6)),
t4: Math.round(Math.pow(10, logMin + logRange * 0.8)),
max
};
})() : null;
return legendThresholds && (
<div style={{
position: "absolute",
bottom: 16,
left: 16,
zIndex: 1000,
backgroundColor: 'rgba(255, 255, 255, 0.9)',
padding: '12px',
borderRadius: '8px',
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
fontSize: '12px',
fontFamily: 'monospace'
}}>
<div style={{ fontWeight: 'bold', marginBottom: '8px' }}>
Path Traffic - Log Scale ({pathConnections.length} connections)
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div style={{ width: '20px', height: '2px', backgroundColor: '#dc2626' }}></div>
<span>High: {legendThresholds.t4}+ packets</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div style={{ width: '20px', height: '2px', backgroundColor: '#ea580c' }}></div>
<span>Med-High: {legendThresholds.t3}-{legendThresholds.t4 - 1}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div style={{ width: '20px', height: '2px', backgroundColor: '#f59e0b' }}></div>
<span>Medium: {legendThresholds.t2}-{legendThresholds.t3 - 1}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div style={{ width: '20px', height: '2px', backgroundColor: '#eab308' }}></div>
<span>Low-Med: {legendThresholds.t1}-{legendThresholds.t2 - 1}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div style={{ width: '20px', height: '2px', backgroundColor: '#84cc16' }}></div>
<span>Low: {legendThresholds.min + 1}-{legendThresholds.t1 - 1}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div style={{ width: '20px', height: '2px', backgroundColor: '#6b7280' }}></div>
<span>Minimal: {legendThresholds.min}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginTop: '4px', paddingTop: '4px', borderTop: '1px solid #e5e7eb' }}>
<div style={{ width: '20px', height: '2px', backgroundColor: '#8b5cf6' }}></div>
<span>Direct connections</span>
</div>
</div>
</div>
);
})()}
</div>
);
}

View File

@@ -4,6 +4,8 @@ import { buildApiUrl } from '@/lib/api';
export interface AllNeighborsConnection {
source_node: string;
target_node: string;
connection_type: string;
packet_count: number;
source_name: string;
source_latitude: number;
source_longitude: number;
@@ -21,6 +23,7 @@ interface UseAllNeighborsParams {
maxLng?: number | null;
nodeTypes?: string[];
lastSeen?: number | null;
region?: string;
enabled?: boolean;
}
@@ -31,10 +34,11 @@ export function useAllNeighbors({
maxLng,
nodeTypes,
lastSeen,
region,
enabled = true
}: UseAllNeighborsParams) {
return useQuery({
queryKey: ['allNeighbors', minLat, maxLat, minLng, maxLng, nodeTypes, lastSeen],
queryKey: ['allNeighbors', minLat, maxLat, minLng, maxLng, nodeTypes, lastSeen, region],
queryFn: async (): Promise<AllNeighborsConnection[]> => {
const params = new URLSearchParams();
@@ -56,6 +60,9 @@ export function useAllNeighbors({
if (lastSeen !== null && lastSeen !== undefined) {
params.append('lastSeen', lastSeen.toString());
}
if (region) {
params.append('region', region);
}
const url = `/api/neighbors/all${params.toString() ? `?${params.toString()}` : ''}`;

View File

@@ -278,7 +278,7 @@ export async function getMeshcoreNodeInfo(publicKey: string, limit: number = 50)
}
}
export async function getAllNodeNeighbors(lastSeen: string | null = null, minLat?: string | null, maxLat?: string | null, minLng?: string | null, maxLng?: string | null, nodeTypes?: string[]) {
export async function getAllNodeNeighbors(lastSeen: string | null = null, minLat?: string | null, maxLat?: string | null, minLng?: string | null, maxLng?: string | null, nodeTypes?: string[], region?: string) {
try {
// Build where conditions for visible nodes
let visibleNodeWhereConditions = [
@@ -321,6 +321,10 @@ export async function getAllNodeNeighbors(lastSeen: string | null = null, minLat
const meshcoreWhere = meshcoreWhereConditions.length > 0 ? `AND ${meshcoreWhereConditions.join(" AND ")}` : '';
// Build region filtering for meshcore_packets
const regionFilter = generateRegionWhereClause(region);
const packetsRegionWhere = regionFilter.whereClause ? `AND ${regionFilter.whereClause}` : '';
const allNeighborsQuery = `
WITH visible_nodes AS (
-- Get only nodes visible on the current map view
@@ -353,11 +357,28 @@ export async function getAllNodeNeighbors(lastSeen: string | null = null, minLat
${meshcoreWhere}
GROUP BY public_key
),
neighbor_connections AS (
repeater_prefixes AS (
-- Get repeater prefixes info, excluding collisions (multiple repeaters per prefix)
-- Only include repeaters from the selected region
SELECT
substring(public_key, 1, 2) as prefix,
count() as node_count,
any(public_key) as representative_key,
any(node_name) as representative_name
FROM meshcore_adverts_latest
WHERE is_repeater = 1
AND last_seen >= now() - INTERVAL 2 DAY
${regionFilter.whereClause ? `AND ${regionFilter.whereClause}` : ''}
GROUP BY prefix
HAVING node_count = 1 -- Only include prefixes with exactly one repeater
),
direct_connections AS (
-- Get all direct connections (path_len = 0) but only between visible nodes
SELECT DISTINCT
hex(origin_pubkey) as source_node,
public_key as target_node
public_key as target_node,
'direct' as connection_type,
1 as packet_count -- Direct connections don't have packet counts, use 1 as default
FROM meshcore_adverts
WHERE path_len = 0
AND hex(origin_pubkey) != public_key
@@ -365,10 +386,82 @@ export async function getAllNodeNeighbors(lastSeen: string | null = null, minLat
AND hex(origin_pubkey) IN (SELECT node_id FROM visible_nodes)
AND public_key IN (SELECT node_id FROM visible_nodes)
${meshcoreWhere}
),
path_neighbors AS (
-- Extract neighbors from routing paths with packet counts
SELECT
source_prefix,
target_prefix,
'path' as connection_type,
count() as packet_count
FROM (
SELECT
upper(hex(substring(path, i, 1))) as source_prefix,
upper(hex(substring(path, i + 1, 1))) as target_prefix
FROM (
SELECT
path,
path_len
FROM meshcore_packets
WHERE path_len >= 2
AND ingest_timestamp >= now() - INTERVAL 1 DAY
${packetsRegionWhere}
) p
ARRAY JOIN range(1, path_len) as i
WHERE i < path_len
) path_pairs
WHERE source_prefix IN (SELECT prefix FROM repeater_prefixes)
AND target_prefix IN (SELECT prefix FROM repeater_prefixes)
AND source_prefix != target_prefix
GROUP BY source_prefix, target_prefix
),
prefix_to_key_map AS (
-- Map prefixes back to full public keys for visible nodes
SELECT
rp.prefix,
rp.representative_key as public_key,
rp.representative_name as node_name
FROM repeater_prefixes rp
WHERE rp.representative_key IN (SELECT node_id FROM visible_nodes)
),
path_connections AS (
-- Convert prefix-based path neighbors to public key connections
-- Include all path connections (no exclusion of direct connections)
SELECT
source_map.public_key as source_node,
target_map.public_key as target_node,
'path' as connection_type,
pn.packet_count
FROM path_neighbors pn
JOIN prefix_to_key_map source_map ON pn.source_prefix = source_map.prefix
JOIN prefix_to_key_map target_map ON pn.target_prefix = target_map.prefix
),
direct_connections_filtered AS (
-- Get direct connections but exclude pairs that already have path connections
SELECT
source_node,
target_node,
connection_type,
packet_count
FROM direct_connections
WHERE (source_node, target_node) NOT IN (
SELECT source_node, target_node FROM path_connections
)
AND (target_node, source_node) NOT IN (
SELECT source_node, target_node FROM path_connections
)
),
neighbor_connections AS (
-- Combine path connections and filtered direct connections (path connections take precedence)
SELECT source_node, target_node, connection_type, packet_count FROM path_connections
UNION ALL
SELECT source_node, target_node, connection_type, packet_count FROM direct_connections_filtered
)
SELECT
connections.source_node,
connections.target_node,
connections.connection_type,
connections.packet_count,
source_details.node_name as source_name,
source_details.latitude as source_latitude,
source_details.longitude as source_longitude,
@@ -388,7 +481,7 @@ export async function getAllNodeNeighbors(lastSeen: string | null = null, minLat
AND source_details.longitude IS NOT NULL
AND target_details.latitude IS NOT NULL
AND target_details.longitude IS NOT NULL
ORDER BY connections.source_node, connections.target_node
ORDER BY connections.connection_type, connections.source_node, connections.target_node
`;
const neighborsResult = await clickhouse.query({
@@ -401,6 +494,8 @@ export async function getAllNodeNeighbors(lastSeen: string | null = null, minLat
return neighbors as Array<{
source_node: string;
target_node: string;
connection_type: string;
packet_count: number;
source_name: string;
source_latitude: number;
source_longitude: number;