diff --git a/src/app/api/map/route.ts b/src/app/api/map/route.ts
index f031a33..d5277c6 100644
--- a/src/app/api/map/route.ts
+++ b/src/app/api/map/route.ts
@@ -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
diff --git a/src/app/api/neighbors/all/route.ts b/src/app/api/neighbors/all/route.ts
index 90e3d10..3c1195a 100644
--- a/src/app/api/neighbors/all/route.ts
+++ b/src/app/api/neighbors/all/route.ts
@@ -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) {
diff --git a/src/components/MapView.tsx b/src/components/MapView.tsx
index de014b6..bf3ad03 100644
--- a/src/components/MapView.tsx
+++ b/src/components/MapView.tsx
@@ -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 (
);
@@ -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 (
@@ -736,6 +795,83 @@ export default function MapView({ target = '_self' }: MapViewProps = {}) {
/>
)}
+
+ {/* 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 && (
+
+
+ Path Traffic - Log Scale ({pathConnections.length} connections)
+
+
+
+
+
High: {legendThresholds.t4}+ packets
+
+
+
+
Med-High: {legendThresholds.t3}-{legendThresholds.t4 - 1}
+
+
+
+
Medium: {legendThresholds.t2}-{legendThresholds.t3 - 1}
+
+
+
+
Low-Med: {legendThresholds.t1}-{legendThresholds.t2 - 1}
+
+
+
+
Low: {legendThresholds.min + 1}-{legendThresholds.t1 - 1}
+
+
+
+
Minimal: {legendThresholds.min}
+
+
+
+
+ );
+ })()}
);
}
\ No newline at end of file
diff --git a/src/hooks/useAllNeighbors.ts b/src/hooks/useAllNeighbors.ts
index 5543389..0eb5032 100644
--- a/src/hooks/useAllNeighbors.ts
+++ b/src/hooks/useAllNeighbors.ts
@@ -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 => {
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()}` : ''}`;
diff --git a/src/lib/clickhouse/actions.ts b/src/lib/clickhouse/actions.ts
index 886ab04..a52d1ba 100644
--- a/src/lib/clickhouse/actions.ts
+++ b/src/lib/clickhouse/actions.ts
@@ -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;