diff --git a/meshexplorer/src/app/(app)/meshcore/node/[publicKey]/page.tsx b/meshexplorer/src/app/(app)/meshcore/node/[publicKey]/page.tsx index f4aaa8f..ba97c2d 100644 --- a/meshexplorer/src/app/(app)/meshcore/node/[publicKey]/page.tsx +++ b/meshexplorer/src/app/(app)/meshcore/node/[publicKey]/page.tsx @@ -9,7 +9,7 @@ import { formatPublicKey } from "@/lib/meshcore"; import { getNameIconLabel } from "@/lib/meshcore-map-nodeutils"; import AdvertDetails from "@/components/AdvertDetails"; import ContactQRCode from "@/components/ContactQRCode"; -import { useConfig, LAST_SEEN_OPTIONS } from "@/components/ConfigContext"; +import { useConfig, LAST_SEEN_OPTIONS, neighborMinConfidenceOf } from "@/components/ConfigContext"; import { useNeighbors, type Neighbor } from "@/hooks/useNeighbors"; import { useNodeData, type NodeData, type NodeInfo, type Advert, type LocationHistory, type MqttInfo, type NodeError } from "@/hooks/useNodeData"; import { ArrowRightEndOnRectangleIcon, ArrowRightStartOnRectangleIcon } from "@heroicons/react/24/outline"; @@ -54,6 +54,34 @@ export default function MeshcoreNodePage() { enabled: !!publicKey }); + // Inferred neighbors from the unified neighbor graph (path/anchor derived), filtered by the user's + // global confidence preference. Exclude 'direct' (already shown above) and any neighbor already in + // the direct list, so this section only adds genuinely inferred links. + const { + data: allEdges = [], + isLoading: inferredLoading + } = useNeighbors({ + nodeId: publicKey, + lastSeen: config.lastSeen, + mode: 'all', + minConfidence: neighborMinConfidenceOf(config), + enabled: !!publicKey + }); + const directKeys = new Set(neighbors.map(n => n.public_key)); + const inferredNeighbors = allEdges.filter(n => n.method !== 'direct' && !directKeys.has(n.public_key)); + const methodLabel = (m?: string) => + m === 'anchor-gateway' || m === 'anchor-origin' ? 'Anchored' + : m === 'path-uniq-3b' ? 'Path (3-byte)' + : m === 'path-uniq-2b' ? 'Path (2-byte)' + : m === 'path-uniq-1b' ? 'Path (1-byte)' + : 'Inferred'; + // One list: directly-heard neighbors first, then inferred ones, each flagged so the card can show + // either direction arrows (direct) or an inference indicator (inferred). + const combinedNeighbors = [ + ...neighbors.map(n => ({ ...n, inferred: false })), + ...inferredNeighbors.map(n => ({ ...n, inferred: true })), + ]; + // Extract error information from TanStack Query error const error = queryError?.error || null; const errorCode = queryError?.code || null; @@ -406,10 +434,10 @@ export default function MeshcoreNodePage() {

- Neighbors ({neighborsLoading ? "..." : neighbors.length}) + Neighbors ({neighborsLoading || inferredLoading ? "..." : combinedNeighbors.length})

- Nodes heard directly by this node + Heard directly, or inferred from routing paths & adverts {config.lastSeen !== null && ( Last {(() => { @@ -421,18 +449,18 @@ export default function MeshcoreNodePage() {

- {neighborsLoading ? ( + {(neighborsLoading || inferredLoading) ? (
Loading neighbors...
- ) : neighbors.length === 0 ? ( + ) : combinedNeighbors.length === 0 ? (
No neighbors found
) : (
- {neighbors.map((neighbor) => ( + {combinedNeighbors.map((neighbor) => (
@@ -480,13 +508,25 @@ export default function MeshcoreNodePage() { Location: {neighbor.latitude.toFixed(4)}, {neighbor.longitude.toFixed(4)}
) || null} - {neighbor.directions && neighbor.directions.length > 0 && ( + {neighbor.inferred ? ( +
+ + Inferred · {methodLabel(neighbor.method)} + + + {Math.round((neighbor.confidence ?? 0) * 100)}% conf + +
+ ) : (neighbor.directions && neighbor.directions.length > 0 && (
Direction: {neighbor.directions.includes('incoming') && } {neighbor.directions.includes('outgoing') && }
- )} + ))}
))} diff --git a/meshexplorer/src/app/api/meshcore/node/[publicKey]/neighbors/route.ts b/meshexplorer/src/app/api/meshcore/node/[publicKey]/neighbors/route.ts index 6f1a4bb..62b6e94 100644 --- a/meshexplorer/src/app/api/meshcore/node/[publicKey]/neighbors/route.ts +++ b/meshexplorer/src/app/api/meshcore/node/[publicKey]/neighbors/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from "next/server"; -import { getMeshcoreNodeNeighbors } from "@/lib/clickhouse/actions"; +import { getMeshcoreNodeNeighbors, getMeshcoreNodeAllEdges } from "@/lib/clickhouse/actions"; export async function GET( req: Request, @@ -9,7 +9,11 @@ export async function GET( const { publicKey: rawPublicKey } = await params; const { searchParams } = new URL(req.url); const lastSeen = searchParams.get("lastSeen"); - + // mode=all -> unified neighbor graph (same edges as the map's "show all neighbors"), filtered by + // minConfidence. Default mode -> the legacy direct-adjacency list (with incoming/outgoing). + const mode = searchParams.get("mode"); + const minConfidence = searchParams.get("minConfidence"); + if (!rawPublicKey) { return NextResponse.json({ error: "Public key is required", @@ -28,8 +32,10 @@ export async function GET( // Normalize public key to uppercase for database query const publicKey = rawPublicKey.toUpperCase(); - const neighbors = await getMeshcoreNodeNeighbors(publicKey, lastSeen); - + const neighbors = mode === "all" + ? await getMeshcoreNodeAllEdges(publicKey, minConfidence ? Number(minConfidence) : 0, lastSeen) + : await getMeshcoreNodeNeighbors(publicKey, lastSeen); + // Check if the parent node exists by trying to get basic node info // This is a lightweight check to ensure the node exists before returning neighbors if (!neighbors || neighbors.length === 0) { diff --git a/meshexplorer/src/components/ConfigContext.tsx b/meshexplorer/src/components/ConfigContext.tsx index ce132df..ff92654 100644 --- a/meshexplorer/src/components/ConfigContext.tsx +++ b/meshexplorer/src/components/ConfigContext.tsx @@ -15,6 +15,7 @@ export type Config = { lastSeen: number | null; // seconds, or null for forever meshcoreKeys?: MeshcoreKey[]; // meshcore private keys selectedRegion?: string; // selected region for chat messages + neighborMinConfidence?: number; // min confidence for derived/inferred neighbor edges (0..1) }; @@ -22,8 +23,27 @@ const DEFAULT_CONFIG: Config = { lastSeen: 604800, // 1 week by default meshcoreKeys: [], // default empty selectedRegion: undefined, // no region selected by default + neighborMinConfidence: 0.5, // "Standard": direct + extended-hash path edges; hides anchored/1-byte }; +// Neighbor-edge confidence tiers, mapped to the minimum `confidence` emitted by +// meshcore_all_neighbor_edges (direct=1.0, 3-byte≈0.8, 2-byte≈0.6, anchored≈0.45, 1-byte≈0.4). +// Anchored edges are geographically inferred (not observed hops) so they rank just above the noisy +// 1-byte tier and are hidden at the default "Standard" threshold. +export const NEIGHBOR_CONFIDENCE_OPTIONS = [ + { value: 1.0, label: "MQTT direct only" }, + { value: 0.7, label: "High (extended hash)" }, + { value: 0.5, label: "Standard" }, + { value: 0.45, label: "Include anchored (lower confidence)" }, + { value: 0, label: "All (include weak 1-byte)" }, +]; + +// Resolve the effective confidence floor (handles older stored configs missing the field). +export function neighborMinConfidenceOf(config: Config | undefined | null): number { + const v = config?.neighborMinConfidence; + return typeof v === "number" ? v : 0.5; +} + export const LAST_SEEN_OPTIONS = [ { value: 1800, label: "30m" }, { value: 3600, label: "1h" }, @@ -68,7 +88,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) { return ( {children} - {open && } + {open && } {keyModalOpen && ( void, onClose: () => void, anchorRef: React.RefObject, onOpenKeyModal: () => void }) { +function ConfigPopover({ config, setConfig, onClose, anchorRef }: { config: Config, setConfig: (c: Config) => void, onClose: () => void, anchorRef: React.RefObject }) { const popoverRef = useRef(null); // Click outside to close @@ -152,12 +172,19 @@ function ConfigPopover({ config, setConfig, onClose, anchorRef, onOpenKeyModal }

-
+ +

+ Minimum confidence for inferred neighbor links (map & node pages) +

); diff --git a/meshexplorer/src/components/MapLayerSettings.tsx b/meshexplorer/src/components/MapLayerSettings.tsx index ce03b93..d004b4e 100644 --- a/meshexplorer/src/components/MapLayerSettings.tsx +++ b/meshexplorer/src/components/MapLayerSettings.tsx @@ -1,6 +1,6 @@ "use client"; import React, { useState, useRef, useEffect } from 'react'; -import { useMapLayerSettings, TILE_LAYERS, NODE_TYPE_OPTIONS, NEIGHBOR_CONFIDENCE_OPTIONS, type MapLayerSettings } from '@/hooks/useMapLayerSettings'; +import { useMapLayerSettings, TILE_LAYERS, NODE_TYPE_OPTIONS, type MapLayerSettings } from '@/hooks/useMapLayerSettings'; import { Square3Stack3DIcon } from '@heroicons/react/24/outline'; interface MapLayerSettingsProps { @@ -189,39 +189,6 @@ export default function MapLayerSettingsComponent({ onSettingsChange }: MapLayer - {/* Neighbor confidence - indented sub-option (subsumes the old "only MQTT" toggle: - "MQTT direct only" is the top tier) */} -
- - -

- Higher tiers show only the most trustworthy links (MQTT-direct, then anchored / extended-hash) -

-
- {/* Minimum packet count - indented sub-option */}