override region for node info page with detected region info

This commit is contained in:
ajvpot
2025-09-11 18:28:12 +02:00
parent 46606abbf9
commit 1dd6781113
6 changed files with 131 additions and 6 deletions

View File

@@ -12,6 +12,7 @@ import { useConfig, LAST_SEEN_OPTIONS } 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";
import { RegionProvider } from "@/contexts/RegionContext";
// Interfaces are now imported from useNodeData hook
@@ -193,10 +194,11 @@ export default function MeshcoreNodePage() {
);
}
const { node, recentAdverts, locationHistory, mqtt } = nodeData;
const { node, recentAdverts, locationHistory, mqtt, region } = nodeData;
return (
<div className="min-h-screen bg-gray-50 dark:bg-neutral-800 py-8">
<RegionProvider region={region}>
<div className="min-h-screen bg-gray-50 dark:bg-neutral-800 py-8">
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="bg-white dark:bg-neutral-900 shadow rounded-lg mb-6">
@@ -214,6 +216,11 @@ export default function MeshcoreNodePage() {
<p className="text-gray-600 dark:text-gray-300 font-mono text-sm">
{formatPublicKey(node.public_key)}
</p>
{region && (
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
Region: <span className="font-medium capitalize">{region}</span>
</p>
)}
<div className="flex flex-wrap gap-2 mt-2">
{node.is_repeater && (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
@@ -491,5 +498,6 @@ export default function MeshcoreNodePage() {
</div>
</div>
</div>
</RegionProvider>
);
}

View File

@@ -8,7 +8,7 @@ import { ArrowsPointingOutIcon, ArrowsPointingInIcon } from "@heroicons/react/24
import PathDisplay from "./PathDisplay";
import { useMeshcoreSearches } from "@/hooks/useMeshcoreSearch";
import type { MeshcoreSearchResult } from "@/hooks/useMeshcoreSearch";
import { useConfig } from "./ConfigContext";
import { useConfigWithRegion } from "@/hooks/useConfigWithRegion";
export interface PathData {
origin: string;
@@ -47,7 +47,7 @@ export default function PathVisualization({
const [showGraph, setShowGraph] = useState(false);
const [graphFullscreen, setGraphFullscreen] = useState(false);
const { config } = useConfig();
const { config } = useConfigWithRegion();
const pathsCount = paths.length;
// Process data for tree visualization

View File

@@ -0,0 +1,26 @@
"use client";
import React, { createContext, useContext, ReactNode } from "react";
interface RegionContextType {
region: string | null;
}
const RegionContext = createContext<RegionContextType | null>(null);
interface RegionProviderProps {
children: ReactNode;
region: string | null;
}
export function RegionProvider({ children, region }: RegionProviderProps) {
return (
<RegionContext.Provider value={{ region }}>
{children}
</RegionContext.Provider>
);
}
export function useRegionContext() {
const context = useContext(RegionContext);
return context;
}

View File

@@ -0,0 +1,25 @@
import { useConfig } from "@/components/ConfigContext";
import { useRegionContext } from "@/contexts/RegionContext";
/**
* Custom hook that combines ConfigContext with RegionContext
* When RegionContext is available, it overrides the selectedRegion from ConfigContext
*/
export function useConfigWithRegion() {
const config = useConfig();
const regionContext = useRegionContext();
// If region context is available, override the selectedRegion
if (regionContext) {
return {
...config,
config: {
...config.config,
selectedRegion: regionContext.region
}
};
}
// Otherwise, return the normal config
return config;
}

View File

@@ -11,6 +11,8 @@ export interface NodeInfo {
is_chat_node: number;
is_room_server: number;
has_name: number;
broker: string | null;
topic: string | null;
first_seen: string;
last_seen: string;
}
@@ -53,6 +55,7 @@ export interface NodeData {
recentAdverts: Advert[];
locationHistory: LocationHistory[];
mqtt: MqttInfo;
region: string | null;
}
export interface NodeError {

View File

@@ -1,6 +1,7 @@
"use server";
import { clickhouse } from "./clickhouse";
import { generateRegionWhereClauseFromArray, generateRegionWhereClause } from "@/lib/regionFilters";
import { getRegionConfig } from "@/lib/regions";
export async function getNodePositions({ minLat, maxLat, minLng, maxLng, nodeTypes, lastSeen }: { minLat?: string | null, maxLat?: string | null, minLng?: string | null, maxLng?: string | null, nodeTypes?: string[], lastSeen?: string | null } = {}) {
try {
@@ -95,6 +96,48 @@ export async function getLatestChatMessages({ limit = 20, before, after, channel
}
}
/**
* Determines the region based on broker and topic information
* @param broker Broker string
* @param topic Topic string
* @returns The detected region name or null if no region matches
*/
function detectRegionFromBrokerTopic(broker: string | null, topic: string | null): string | null {
if (!broker || !topic) return null;
// Check each region configuration
const regions = ['seattle', 'portland', 'boston'];
for (const regionName of regions) {
const regionConfig = getRegionConfig(regionName);
if (!regionConfig) continue;
// Check if this topic/broker combination matches the region
if (broker === regionConfig.broker && regionConfig.topics.includes(topic)) {
return regionName;
}
}
return null;
}
/**
* Combined region detection that tries MQTT topics first, then advert data
* @param mqttTopics Array of MQTT topic information
* @param advertBroker Broker from advert data
* @param advertTopic Topic from advert data
* @returns The detected region name or null if no region matches
*/
function detectRegion(mqttTopics: Array<{ topic: string; broker: string }>, advertBroker: string | null, advertTopic: string | null): string | null {
// First try MQTT topics (more reliable for uplinked nodes)
for (const mqttTopic of mqttTopics) {
const region = detectRegionFromBrokerTopic(mqttTopic.broker, mqttTopic.topic);
if (region) return region;
}
// Fallback to advert data (works for non-uplinked nodes)
return detectRegionFromBrokerTopic(advertBroker, advertTopic);
}
export async function getMeshcoreNodeInfo(publicKey: string, limit: number = 50) {
try {
// Get basic node info from the latest advert and first seen time
@@ -109,6 +152,8 @@ export async function getMeshcoreNodeInfo(publicKey: string, limit: number = 50)
argMax(is_chat_node, ingest_timestamp) as is_chat_node,
argMax(is_room_server, ingest_timestamp) as is_room_server,
argMax(has_name, ingest_timestamp) as has_name,
argMax(broker, ingest_timestamp) as broker,
argMax(topic, ingest_timestamp) as topic,
max(ingest_timestamp) as last_seen,
min(ingest_timestamp) as first_seen
FROM meshcore_adverts
@@ -122,7 +167,21 @@ export async function getMeshcoreNodeInfo(publicKey: string, limit: number = 50)
query_params: { publicKey },
format: 'JSONEachRow'
});
const nodeInfo = await nodeInfoResult.json();
const nodeInfo = await nodeInfoResult.json() as Array<{
public_key: string;
node_name: string;
latitude: number | null;
longitude: number | null;
has_location: number;
is_repeater: number;
is_chat_node: number;
is_room_server: number;
has_name: number;
broker: string | null;
topic: string | null;
last_seen: string;
first_seen: string;
}>;
if (!nodeInfo || nodeInfo.length === 0) {
return null;
@@ -232,6 +291,9 @@ export async function getMeshcoreNodeInfo(publicKey: string, limit: number = 50)
const hasPackets = mqttTopics.length > 0;
const isUplinked = mqttTopics.some(topic => topic.is_recent);
// Detect region from MQTT topics and advert data
const detectedRegion = detectRegion(mqttTopics, nodeInfo[0].broker, nodeInfo[0].topic);
return {
node: nodeInfo[0],
recentAdverts: adverts,
@@ -240,7 +302,8 @@ export async function getMeshcoreNodeInfo(publicKey: string, limit: number = 50)
is_uplinked: isUplinked,
has_packets: hasPackets,
topics: mqttTopics
}
},
region: detectedRegion
};
} catch (error) {
console.error('ClickHouse error in getMeshcoreNodeInfo:', error);