mirror of
https://github.com/ajvpot/meshexplorer.git
synced 2026-03-28 17:42:58 +01:00
override region for node info page with detected region info
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
26
src/contexts/RegionContext.tsx
Normal file
26
src/contexts/RegionContext.tsx
Normal 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;
|
||||
}
|
||||
25
src/hooks/useConfigWithRegion.ts
Normal file
25
src/hooks/useConfigWithRegion.ts
Normal 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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user