Fixes for gateway display

This commit is contained in:
Daniel Pupius
2025-04-25 09:54:36 -07:00
parent 4a41b0062d
commit 5b5dad7a68
7 changed files with 181 additions and 106 deletions
+1 -1
View File
@@ -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"
+56 -52
View File
@@ -21,22 +21,13 @@ export const GatewayList: React.FC = () => {
return b.lastHeard - a.lastHeard;
});
if (gatewayArray.length === 0) {
return (
<div className="p-6 text-neutral-400 text-center border border-neutral-700 rounded bg-neutral-800/50">
<div className="flex items-center justify-center mb-2">
<RefreshCw className="w-5 h-5 mr-2 animate-spin" />
</div>
No gateways discovered yet. Waiting for data...
</div>
);
}
// Instead of early return, we'll handle empty state in the JSX now
return (
<div className="space-y-1">
<div className="flex justify-between items-center mb-2">
<h2 className="text-lg font-semibold text-neutral-200">
Mesh Gateways
Gateways
</h2>
<div className="text-sm text-neutral-400 bg-neutral-800/70 px-2 py-0.5 rounded">
{gatewayArray.length}{" "}
@@ -44,48 +35,61 @@ export const GatewayList: React.FC = () => {
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 3xl:grid-cols-5 gap-2">
{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 ? (
<div className="bg-neutral-800/50 hover:bg-neutral-800 p-2 rounded-lg flex items-center">
<div className="p-1.5 rounded-full bg-neutral-700/30 text-neutral-500 mr-2">
<RefreshCw className="w-4 h-4 animate-spin" />
</div>
<div className="flex-1 min-w-0">
<div className="font-medium text-sm text-neutral-400 truncate">
Waiting for gateway data...
</div>
</div>
</div>
) : (
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 (
<MeshCard
key={gateway.gatewayId}
type="gateway"
nodeId={nodeId}
nodeData={matchingNode || {
nodeId,
lastHeard: gateway.lastHeard,
messageCount: gateway.messageCount,
textMessageCount: gateway.textMessageCount
}}
gatewayId={gateway.gatewayId}
observedNodes={gateway.observedNodes}
onClick={handleNodeClick}
isRecent={isRecent}
isActive={isActive}
lastHeard={gateway.lastHeard}
/>
);
})}
const handleNodeClick = (clickedNodeId: number) => {
navigate({ to: "/node/$nodeId", params: { nodeId: clickedNodeId.toString(16) } });
};
return (
<MeshCard
key={gateway.gatewayId}
type="gateway"
nodeId={nodeId}
nodeData={matchingNode || {
nodeId,
lastHeard: gateway.lastHeard,
messageCount: gateway.messageCount,
textMessageCount: gateway.textMessageCount
}}
gatewayId={gateway.gatewayId}
observedNodes={gateway.observedNodes}
onClick={handleNodeClick}
isRecent={isRecent}
isActive={isActive}
lastHeard={gateway.lastHeard}
/>
);
})
)}
</div>
</div>
);
};
};
+63 -14
View File
@@ -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<NodeDetailProps> = ({ 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<NodeDetailProps> = ({ 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<NodeDetailProps> = ({ nodeId }) => {
<ArrowLeft className="w-5 h-5" />
</button>
<div className={`p-2 mr-3 rounded-full ${isActive ? 'bg-green-900/30 text-green-500' : 'bg-neutral-700/30 text-neutral-500'}`}>
<Radio className="w-5 h-5" />
{node.isGateway ? <Signal className="w-5 h-5" /> : <Radio className="w-5 h-5" />}
</div>
<div className="flex-1 flex flex-col md:flex-row md:items-center">
<h1 className="text-xl font-semibold text-neutral-200 mr-3">{nodeName}</h1>
@@ -302,6 +328,22 @@ export const NodeDetail: React.FC<NodeDetailProps> = ({ nodeId }) => {
<div className="bg-neutral-800/50 p-4 rounded-lg">
<h2 className="font-semibold mb-4 text-neutral-300 border-b border-neutral-700 pb-2">Device Information</h2>
<div className="space-y-3">
{/* Display gateway info if this is a gateway */}
{node.isGateway && (
<div className="flex justify-between items-center mb-2 bg-blue-900/20 p-2 rounded">
<span className="text-blue-400 flex items-center">
<Signal className="w-4 h-4 mr-1.5" />
Gateway Node
</span>
{node.observedNodeCount !== undefined && (
<span className="text-blue-400 flex items-center">
<Users className="w-4 h-4 mr-1.5" />
{node.observedNodeCount} {node.observedNodeCount === 1 ? 'node' : 'nodes'}
</span>
)}
</div>
)}
{node.longName && (
<div className="flex justify-between items-center">
<span className="text-neutral-400">Name:</span>
@@ -390,14 +432,21 @@ export const NodeDetail: React.FC<NodeDetailProps> = ({ nodeId }) => {
<span className="text-neutral-400">Gateways:</span>
<div className="flex flex-col items-end">
{node.gatewayId ? (
<Link
to="/node/$nodeId"
params={{ nodeId: node.gatewayId.substring(1) }}
className="text-neutral-200 font-mono text-xs truncate max-w-[180px] hover:text-blue-400 transition-colors flex items-center"
>
{node.gatewayId}
<ChevronRight className="w-4 h-4 text-neutral-500 ml-1" />
</Link>
// Check if gateway ID matches the current node ID (self-reporting)
node.gatewayId === `!${nodeId.toString(16).toLowerCase()}` ? (
<span className="text-emerald-400 text-xs flex items-center">
Self reported
</span>
) : (
<Link
to="/node/$nodeId"
params={{ nodeId: node.gatewayId.substring(1) }}
className="text-neutral-200 font-mono text-xs truncate max-w-[180px] hover:text-blue-400 transition-colors flex items-center"
>
{node.gatewayId}
<ChevronRight className="w-4 h-4 text-neutral-500 ml-1" />
</Link>
)
) : (
<span className="text-neutral-400 italic">None detected</span>
)}
+56 -37
View File
@@ -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<number>();
// 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 (
<div className="p-6 text-neutral-400 text-center border border-neutral-700 rounded bg-neutral-800/50">
<div className="flex items-center justify-center mb-2">
<RefreshCw className="w-5 h-5 mr-2 animate-spin" />
</div>
No nodes discovered yet. Waiting for data...
</div>
);
}
// Instead of early return, we'll handle the empty state in the JSX
return (
<div className="space-y-1">
<div className="flex justify-between items-center mb-2">
<h2 className="text-lg font-semibold text-neutral-200">Mesh Nodes</h2>
<h2 className="text-lg font-semibold text-neutral-200">Nodes</h2>
<div className="text-sm text-neutral-400 bg-neutral-800/70 px-2 py-0.5 rounded">
{nodeArray.length} {nodeArray.length === 1 ? "node" : "nodes"}
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 3xl:grid-cols-5 gap-2">
{sortedNodes.map((node) => {
// Calculate time since last heard (in seconds)
const secondsSinceLastHeard = Date.now() / 1000 - node.lastHeard;
{nodeArray.length === 0 ? (
<div className="bg-neutral-800/50 hover:bg-neutral-800 p-2 rounded-lg flex items-center">
<div className="p-1.5 rounded-full bg-neutral-700/30 text-neutral-500 mr-2">
<RefreshCw className="w-4 h-4 animate-spin" />
</div>
<div className="flex-1 min-w-0">
<div className="font-medium text-sm text-neutral-400 truncate">
{Object.keys(nodes).length > 0 ?
"All nodes are shown as gateways above" :
"Waiting for node data..."}
</div>
</div>
</div>
) : (
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 (
<MeshCard
key={node.nodeId}
type="node"
nodeId={node.nodeId}
nodeData={node}
onClick={handleNodeClick}
isRecent={isRecent}
isActive={isActive}
lastHeard={node.lastHeard}
/>
);
})}
return (
<MeshCard
key={node.nodeId}
type="node"
nodeId={node.nodeId}
nodeData={node}
onClick={handleNodeClick}
isRecent={isRecent}
isActive={isActive}
lastHeard={node.lastHeard}
/>
);
})
)}
</div>
</div>
);
};
};
+1 -1
View File
@@ -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(
+1 -1
View File
@@ -9,7 +9,7 @@ function HomePage() {
return (
<PageWrapper>
<div className="mb-4">
<h1 className="text-xl font-bold mb-2 text-neutral-200">Network Dashboard</h1>
<h1 className="text-xl font-bold mb-2 text-neutral-200">Mesh Overview</h1>
<p className="text-sm text-neutral-400">
Real-time view of your Meshtastic mesh network traffic
</p>
+3
View File
@@ -25,6 +25,9 @@ export interface NodeData {
textMessageCount: number;
channelId?: string;
gatewayId?: string;
// Fields for gateway nodes
isGateway?: boolean;
observedNodeCount?: number;
}
export interface TextMessage {