mirror of
https://github.com/dpup/meshstream.git
synced 2026-06-29 14:32:02 +02:00
Fixes for gateway display
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -25,6 +25,9 @@ export interface NodeData {
|
||||
textMessageCount: number;
|
||||
channelId?: string;
|
||||
gatewayId?: string;
|
||||
// Fields for gateway nodes
|
||||
isGateway?: boolean;
|
||||
observedNodeCount?: number;
|
||||
}
|
||||
|
||||
export interface TextMessage {
|
||||
|
||||
Reference in New Issue
Block a user