diff --git a/src/app/(app)/meshcore/node/[publicKey]/page.tsx b/src/app/(app)/meshcore/node/[publicKey]/page.tsx
index 2d52d01..8fb7f79 100644
--- a/src/app/(app)/meshcore/node/[publicKey]/page.tsx
+++ b/src/app/(app)/meshcore/node/[publicKey]/page.tsx
@@ -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 (
-
+
+
{/* Header */}
@@ -214,6 +216,11 @@ export default function MeshcoreNodePage() {
{formatPublicKey(node.public_key)}
+ {region && (
+
+ Region: {region}
+
+ )}
{node.is_repeater && (
@@ -491,5 +498,6 @@ export default function MeshcoreNodePage() {
+
);
}
diff --git a/src/components/PathVisualization.tsx b/src/components/PathVisualization.tsx
index bb5c809..b8147a1 100644
--- a/src/components/PathVisualization.tsx
+++ b/src/components/PathVisualization.tsx
@@ -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
diff --git a/src/contexts/RegionContext.tsx b/src/contexts/RegionContext.tsx
new file mode 100644
index 0000000..950fc44
--- /dev/null
+++ b/src/contexts/RegionContext.tsx
@@ -0,0 +1,26 @@
+"use client";
+import React, { createContext, useContext, ReactNode } from "react";
+
+interface RegionContextType {
+ region: string | null;
+}
+
+const RegionContext = createContext
(null);
+
+interface RegionProviderProps {
+ children: ReactNode;
+ region: string | null;
+}
+
+export function RegionProvider({ children, region }: RegionProviderProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+export function useRegionContext() {
+ const context = useContext(RegionContext);
+ return context;
+}
diff --git a/src/hooks/useConfigWithRegion.ts b/src/hooks/useConfigWithRegion.ts
new file mode 100644
index 0000000..3160c53
--- /dev/null
+++ b/src/hooks/useConfigWithRegion.ts
@@ -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;
+}
diff --git a/src/hooks/useNodeData.ts b/src/hooks/useNodeData.ts
index 52210bc..226e8ff 100644
--- a/src/hooks/useNodeData.ts
+++ b/src/hooks/useNodeData.ts
@@ -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 {
diff --git a/src/lib/clickhouse/actions.ts b/src/lib/clickhouse/actions.ts
index a88fcee..a4793d4 100644
--- a/src/lib/clickhouse/actions.ts
+++ b/src/lib/clickhouse/actions.ts
@@ -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);