diff --git a/main.go b/main.go index 5173fb2..3acbb63 100644 --- a/main.go +++ b/main.go @@ -17,7 +17,7 @@ const ( mqttBroker = "mqtt.bayme.sh" mqttUsername = "meshdev" mqttPassword = "large4cats" - mqttTopicPrefix = "msh/US/CA/Motherlode" + mqttTopicPrefix = "msh/US/bayarea" // Web server configuration serverHost = "localhost" diff --git a/web/src/components/dashboard/GatewayList.tsx b/web/src/components/dashboard/GatewayList.tsx index 1ca94c3..30ead31 100644 --- a/web/src/components/dashboard/GatewayList.tsx +++ b/web/src/components/dashboard/GatewayList.tsx @@ -21,22 +21,13 @@ export const GatewayList: React.FC = () => { return b.lastHeard - a.lastHeard; }); - if (gatewayArray.length === 0) { - return ( -
-
- -
- No gateways discovered yet. Waiting for data... -
- ); - } + // Instead of early return, we'll handle empty state in the JSX now return (

- Mesh Gateways + Gateways

{gatewayArray.length}{" "} @@ -44,48 +35,61 @@ export const GatewayList: React.FC = () => {
- {sortedGateways.map((gateway) => { - // Extract node ID from gateway ID format if possible - const nodeIdMatch = gateway.gatewayId.match(/^!([0-9a-f]+)/i); - let nodeId = 0; - let matchingNode = null; - - if (nodeIdMatch) { - const nodeIdHex = nodeIdMatch[1]; - nodeId = parseInt(nodeIdHex, 16); - matchingNode = nodes[nodeId]; - } - - // Determine if gateway is active (using stricter timeframe for gateways) - const secondsSinceLastHeard = Date.now() / 1000 - gateway.lastHeard; - const isRecent = secondsSinceLastHeard < 300; // 5 minutes for gateways - const isActive = !isRecent && secondsSinceLastHeard < 900; // 5-15 minutes for gateways + {gatewayArray.length === 0 ? ( +
+
+ +
+
+
+ Waiting for gateway data... +
+
+
+ ) : ( + sortedGateways.map((gateway) => { + // Extract node ID from gateway ID format if possible + const nodeIdMatch = gateway.gatewayId.match(/^!([0-9a-f]+)/i); + let nodeId = 0; + let matchingNode = null; + + if (nodeIdMatch) { + const nodeIdHex = nodeIdMatch[1]; + nodeId = parseInt(nodeIdHex, 16); + matchingNode = nodes[nodeId]; + } + + // Determine if gateway is active (using stricter timeframe for gateways) + const secondsSinceLastHeard = Date.now() / 1000 - gateway.lastHeard; + const isRecent = secondsSinceLastHeard < 300; // 5 minutes for gateways + const isActive = !isRecent && secondsSinceLastHeard < 900; // 5-15 minutes for gateways - const handleNodeClick = (clickedNodeId: number) => { - navigate({ to: "/node/$nodeId", params: { nodeId: clickedNodeId.toString(16) } }); - }; - - return ( - - ); - })} + const handleNodeClick = (clickedNodeId: number) => { + navigate({ to: "/node/$nodeId", params: { nodeId: clickedNodeId.toString(16) } }); + }; + + return ( + + ); + }) + )}
); -}; +}; \ No newline at end of file diff --git a/web/src/components/dashboard/NodeDetail.tsx b/web/src/components/dashboard/NodeDetail.tsx index bca202e..8e8e40d 100644 --- a/web/src/components/dashboard/NodeDetail.tsx +++ b/web/src/components/dashboard/NodeDetail.tsx @@ -3,10 +3,10 @@ import { useNavigate, Link } from "@tanstack/react-router"; import { useAppSelector, useAppDispatch } from "../../hooks"; import { selectNode } from "../../store/slices/aggregatorSlice"; import { - ArrowLeft, Radio, Battery, Cpu, Thermometer, Gauge, Signal, + ArrowLeft, Radio, Cpu, Thermometer, Gauge, Signal, Droplets, Map, Calendar, Clock, Wifi, BarChart, BatteryFull, BatteryMedium, BatteryLow, AlertTriangle, - Zap, Timer, ChevronRight + Zap, Timer, ChevronRight, Users } from "lucide-react"; interface NodeDetailProps { @@ -199,8 +199,34 @@ const formatUptime = (seconds: number): string => { export const NodeDetail: React.FC = ({ nodeId }) => { const dispatch = useAppDispatch(); const navigate = useNavigate(); - const { nodes } = useAppSelector((state) => state.aggregator); - const node = nodes[nodeId]; + const { nodes, gateways } = useAppSelector((state) => state.aggregator); + + // First try to get the node directly from nodes collection + let node = nodes[nodeId]; + + // If node not found in nodes collection, check if it might be a gateway + if (!node) { + // Construct the gateway ID format from the node ID + const gatewayId = `!${nodeId.toString(16).toLowerCase()}`; + + // Check if there's a gateway with this ID + const gateway = gateways[gatewayId]; + + if (gateway) { + // Create a synthetic node from the gateway data + node = { + nodeId, + lastHeard: gateway.lastHeard, + messageCount: gateway.messageCount, + textMessageCount: gateway.textMessageCount, + // Mark this as a gateway node + isGateway: true, + gatewayId: gatewayId, + // Add observed nodes info + observedNodeCount: gateway.observedNodes.length + }; + } + } useEffect(() => { // Update selected node in the store @@ -235,7 +261,7 @@ export const NodeDetail: React.FC = ({ nodeId }) => { } // Format node name - const nodeName = node.shortName || node.longName || `Node ${nodeId.toString(16)}`; + const nodeName = node.shortName || node.longName || `${node.isGateway ? 'Gateway' : 'Node'} ${nodeId.toString(16)}`; // Format timestamps const lastHeardDate = new Date(node.lastHeard * 1000); @@ -281,7 +307,7 @@ export const NodeDetail: React.FC = ({ nodeId }) => {
- + {node.isGateway ? : }

{nodeName}

@@ -302,6 +328,22 @@ export const NodeDetail: React.FC = ({ nodeId }) => {

Device Information

+ {/* Display gateway info if this is a gateway */} + {node.isGateway && ( +
+ + + Gateway Node + + {node.observedNodeCount !== undefined && ( + + + {node.observedNodeCount} {node.observedNodeCount === 1 ? 'node' : 'nodes'} + + )} +
+ )} + {node.longName && (
Name: @@ -390,14 +432,21 @@ export const NodeDetail: React.FC = ({ nodeId }) => { Gateways:
{node.gatewayId ? ( - - {node.gatewayId} - - + // Check if gateway ID matches the current node ID (self-reporting) + node.gatewayId === `!${nodeId.toString(16).toLowerCase()}` ? ( + + Self reported + + ) : ( + + {node.gatewayId} + + + ) ) : ( None detected )} diff --git a/web/src/components/dashboard/NodeList.tsx b/web/src/components/dashboard/NodeList.tsx index 6ffec26..c5f126a 100644 --- a/web/src/components/dashboard/NodeList.tsx +++ b/web/src/components/dashboard/NodeList.tsx @@ -5,11 +5,24 @@ import { RefreshCw } from "lucide-react"; import { MeshCard } from "./MeshCard"; export const NodeList: React.FC = () => { - const { nodes } = useAppSelector((state) => state.aggregator); + const { nodes, gateways } = useAppSelector((state) => state.aggregator); const navigate = useNavigate(); - // Convert nodes object to array for sorting - const nodeArray = Object.values(nodes); + // Create a set of node IDs that are already shown as gateways + const gatewayNodeIds = new Set(); + + // Extract node IDs from gateway IDs + Object.keys(gateways).forEach(gatewayId => { + const nodeIdMatch = gatewayId.match(/^!([0-9a-f]+)/i); + if (nodeIdMatch) { + const nodeIdHex = nodeIdMatch[1]; + const nodeId = parseInt(nodeIdHex, 16); + gatewayNodeIds.add(nodeId); + } + }); + + // Convert nodes object to array and filter out gateway nodes + const nodeArray = Object.values(nodes).filter(node => !gatewayNodeIds.has(node.nodeId)); // Sort by node ID (stable) const sortedNodes = nodeArray.sort((a, b) => a.nodeId - b.nodeId); @@ -18,51 +31,57 @@ export const NodeList: React.FC = () => { navigate({ to: "/node/$nodeId", params: { nodeId: nodeId.toString(16) } }); }; - if (nodeArray.length === 0) { - return ( -
-
- -
- No nodes discovered yet. Waiting for data... -
- ); - } + // Instead of early return, we'll handle the empty state in the JSX return (
-

Mesh Nodes

+

Nodes

{nodeArray.length} {nodeArray.length === 1 ? "node" : "nodes"}
- {sortedNodes.map((node) => { - // Calculate time since last heard (in seconds) - const secondsSinceLastHeard = Date.now() / 1000 - node.lastHeard; + {nodeArray.length === 0 ? ( +
+
+ +
+
+
+ {Object.keys(nodes).length > 0 ? + "All nodes are shown as gateways above" : + "Waiting for node data..."} +
+
+
+ ) : ( + sortedNodes.map((node) => { + // Calculate time since last heard (in seconds) + const secondsSinceLastHeard = Date.now() / 1000 - node.lastHeard; - // Determine node activity status: - // Recent: < 10 minutes (green) - // Active: 10-30 minutes (blue) - // Inactive: > 30 minutes (grey) - const isRecent = secondsSinceLastHeard < 600; // 10 minutes - const isActive = !isRecent && secondsSinceLastHeard < 1800; // 10-30 minutes + // Determine node activity status: + // Recent: < 10 minutes (green) + // Active: 10-30 minutes (blue) + // Inactive: > 30 minutes (grey) + const isRecent = secondsSinceLastHeard < 600; // 10 minutes + const isActive = !isRecent && secondsSinceLastHeard < 1800; // 10-30 minutes - return ( - - ); - })} + return ( + + ); + }) + )}
); -}; +}; \ No newline at end of file diff --git a/web/src/routes/__root.tsx b/web/src/routes/__root.tsx index a479bdb..2116e04 100644 --- a/web/src/routes/__root.tsx +++ b/web/src/routes/__root.tsx @@ -22,7 +22,7 @@ function RootLayout() { const isReconnectingRef = useRef(false); useEffect(() => { - console.log("[SSE] Setting up event source (should happen only once)"); + console.log("[SSE] Setting up event source"); // Set up Server-Sent Events connection const cleanup = streamPackets( diff --git a/web/src/routes/home.tsx b/web/src/routes/home.tsx index 8527247..3b99fcc 100644 --- a/web/src/routes/home.tsx +++ b/web/src/routes/home.tsx @@ -9,7 +9,7 @@ function HomePage() { return (
-

Network Dashboard

+

Mesh Overview

Real-time view of your Meshtastic mesh network traffic

diff --git a/web/src/store/slices/aggregatorSlice.ts b/web/src/store/slices/aggregatorSlice.ts index 74550ec..ffd43c6 100644 --- a/web/src/store/slices/aggregatorSlice.ts +++ b/web/src/store/slices/aggregatorSlice.ts @@ -25,6 +25,9 @@ export interface NodeData { textMessageCount: number; channelId?: string; gatewayId?: string; + // Fields for gateway nodes + isGateway?: boolean; + observedNodeCount?: number; } export interface TextMessage {