mirror of
https://github.com/dpup/meshstream.git
synced 2026-03-28 17:42:37 +01:00
Tweak gateway visualization and card headers
This commit is contained in:
@@ -2,7 +2,12 @@ import React, { useEffect } from "react";
|
|||||||
import { useNavigate, Link } from "@tanstack/react-router";
|
import { useNavigate, Link } from "@tanstack/react-router";
|
||||||
import { useAppSelector, useAppDispatch } from "../../hooks";
|
import { useAppSelector, useAppDispatch } from "../../hooks";
|
||||||
import { selectNode } from "../../store/slices/aggregatorSlice";
|
import { selectNode } from "../../store/slices/aggregatorSlice";
|
||||||
import { getActivityLevel, getNodeColors, getStatusText, formatLastSeen } from "../../lib/activity";
|
import {
|
||||||
|
getActivityLevel,
|
||||||
|
getNodeColors,
|
||||||
|
getStatusText,
|
||||||
|
formatLastSeen,
|
||||||
|
} from "../../lib/activity";
|
||||||
import { cn } from "../../lib/cn";
|
import { cn } from "../../lib/cn";
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
@@ -42,6 +47,7 @@ import {
|
|||||||
formatUptime,
|
formatUptime,
|
||||||
getRegionName,
|
getRegionName,
|
||||||
getModemPresetName,
|
getModemPresetName,
|
||||||
|
getNodeDisplayName,
|
||||||
} from "../../utils/formatters";
|
} from "../../utils/formatters";
|
||||||
|
|
||||||
// Format role string for display
|
// Format role string for display
|
||||||
@@ -66,11 +72,11 @@ const copyToClipboard = async (text: string) => {
|
|||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text);
|
||||||
} catch {
|
} catch {
|
||||||
// Fallback for older browsers
|
// Fallback for older browsers
|
||||||
const textArea = document.createElement('textarea');
|
const textArea = document.createElement("textarea");
|
||||||
textArea.value = text;
|
textArea.value = text;
|
||||||
document.body.appendChild(textArea);
|
document.body.appendChild(textArea);
|
||||||
textArea.select();
|
textArea.select();
|
||||||
document.execCommand('copy');
|
document.execCommand("copy");
|
||||||
document.body.removeChild(textArea);
|
document.body.removeChild(textArea);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -88,13 +94,13 @@ export const NodeDetail: React.FC<NodeDetailProps> = ({ nodeId }) => {
|
|||||||
|
|
||||||
// Construct the gateway ID format from the node ID
|
// Construct the gateway ID format from the node ID
|
||||||
const gatewayId = `!${nodeId.toString(16).toLowerCase()}`;
|
const gatewayId = `!${nodeId.toString(16).toLowerCase()}`;
|
||||||
|
|
||||||
// Check if there's a gateway with this ID
|
// Check if there's a gateway with this ID
|
||||||
const gateway = gateways[gatewayId];
|
const gateway = gateways[gatewayId];
|
||||||
|
|
||||||
// First try to get the node directly from nodes collection
|
// First try to get the node directly from nodes collection
|
||||||
let node = nodes[nodeId];
|
let node = nodes[nodeId];
|
||||||
|
|
||||||
// If node exists but doesn't have isGateway set, check if it should be a gateway
|
// If node exists but doesn't have isGateway set, check if it should be a gateway
|
||||||
if (node && !node.isGateway && gateway) {
|
if (node && !node.isGateway && gateway) {
|
||||||
// Update the node with gateway info
|
// Update the node with gateway info
|
||||||
@@ -105,7 +111,7 @@ export const NodeDetail: React.FC<NodeDetailProps> = ({ nodeId }) => {
|
|||||||
observedNodeCount: gateway.observedNodes.length,
|
observedNodeCount: gateway.observedNodes.length,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// If node not found in nodes collection, create a synthetic node from gateway data
|
// If node not found in nodes collection, create a synthetic node from gateway data
|
||||||
if (!node && gateway) {
|
if (!node && gateway) {
|
||||||
// Create a synthetic node from the gateway data
|
// Create a synthetic node from the gateway data
|
||||||
@@ -167,7 +173,7 @@ export const NodeDetail: React.FC<NodeDetailProps> = ({ nodeId }) => {
|
|||||||
|
|
||||||
// Calculate how recently node was active
|
// Calculate how recently node was active
|
||||||
const secondsAgo = Math.floor(Date.now() / 1000) - node.lastHeard;
|
const secondsAgo = Math.floor(Date.now() / 1000) - node.lastHeard;
|
||||||
|
|
||||||
// Use activity helpers
|
// Use activity helpers
|
||||||
const activityLevel = getActivityLevel(node.lastHeard, node.isGateway);
|
const activityLevel = getActivityLevel(node.lastHeard, node.isGateway);
|
||||||
const activityColors = getNodeColors(activityLevel, node.isGateway);
|
const activityColors = getNodeColors(activityLevel, node.isGateway);
|
||||||
@@ -226,15 +232,20 @@ export const NodeDetail: React.FC<NodeDetailProps> = ({ nodeId }) => {
|
|||||||
</h1>
|
</h1>
|
||||||
<div className="text-sm text-neutral-400 flex items-center">
|
<div className="text-sm text-neutral-400 flex items-center">
|
||||||
<span
|
<span
|
||||||
className={cn("inline-block w-2 h-2 rounded-full mr-2", activityColors.statusDot)}
|
className={cn(
|
||||||
|
"inline-block w-2 h-2 rounded-full mr-2",
|
||||||
|
activityColors.statusDot
|
||||||
|
)}
|
||||||
></span>
|
></span>
|
||||||
{statusText} - last seen {lastSeenText}
|
{statusText} - last seen {lastSeenText}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={cn(
|
<div
|
||||||
"text-sm bg-neutral-900/50 px-3 py-1.5 rounded font-mono effect-inset tracking-wider",
|
className={cn(
|
||||||
activityColors.textClass
|
"text-sm bg-neutral-900/50 px-3 py-1.5 rounded font-mono effect-inset tracking-wider",
|
||||||
)}>
|
activityColors.textClass
|
||||||
|
)}
|
||||||
|
>
|
||||||
!{nodeId.toString(16)}
|
!{nodeId.toString(16)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -333,23 +344,60 @@ export const NodeDetail: React.FC<NodeDetailProps> = ({ nodeId }) => {
|
|||||||
{/* Show MapReport-specific information for gateways */}
|
{/* Show MapReport-specific information for gateways */}
|
||||||
{node.isGateway && (
|
{node.isGateway && (
|
||||||
<div className="mt-4 pt-3 border-t border-neutral-700 space-y-3">
|
<div className="mt-4 pt-3 border-t border-neutral-700 space-y-3">
|
||||||
<div className="flex justify-between items-center mb-2 p-2 rounded effect-inset bg-neutral-700/50 ">
|
<div className=" mb-2 p-2 rounded effect-inset bg-neutral-700/50 ">
|
||||||
<span className="flex items-center">
|
<div className="flex justify-between items-center">
|
||||||
<Network className="w-4 h-4 mr-1.5" />
|
|
||||||
Gateway Node
|
|
||||||
</span>
|
|
||||||
{node.observedNodeCount !== undefined && (
|
|
||||||
<span className="flex items-center">
|
<span className="flex items-center">
|
||||||
<Users className="w-4 h-4 mr-1.5" />
|
<Network className="w-4 h-4 mr-1.5" />
|
||||||
{node.observedNodeCount}{" "}
|
Gateway Node
|
||||||
{node.observedNodeCount === 1 ? "node" : "nodes"}
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
{node.observedNodeCount !== undefined && (
|
||||||
{node.mapReport?.numOnlineLocalNodes !== undefined && (
|
<span className="flex items-center">
|
||||||
<span className="text-xs flex items-center font-mono opacity-80">
|
<Users className="w-4 h-4 mr-1.5" />
|
||||||
{node.mapReport.numOnlineLocalNodes} online local nodes
|
{node.observedNodeCount}{" "}
|
||||||
</span>
|
{node.observedNodeCount === 1 ? "node" : "nodes"}
|
||||||
)}
|
</span>
|
||||||
|
)}
|
||||||
|
{node.mapReport?.numOnlineLocalNodes !== undefined && (
|
||||||
|
<span className="text-xs flex items-center font-mono opacity-80">
|
||||||
|
{node.mapReport.numOnlineLocalNodes} online local nodes
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Observed Nodes Grid - integrated into Gateway Node section */}
|
||||||
|
{gateway?.observedNodes &&
|
||||||
|
gateway.observedNodes.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<div className="my-2 text-xs text-neutral-400">
|
||||||
|
Recently observed nodes:
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
|
||||||
|
{gateway.observedNodes.map((observedNodeId) => {
|
||||||
|
const observedNode = nodes[observedNodeId];
|
||||||
|
const displayName = getNodeDisplayName(
|
||||||
|
observedNodeId,
|
||||||
|
observedNode
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={observedNodeId}
|
||||||
|
to="/node/$nodeId"
|
||||||
|
params={{
|
||||||
|
nodeId: observedNodeId
|
||||||
|
.toString(16)
|
||||||
|
.toLowerCase(),
|
||||||
|
}}
|
||||||
|
className="flex items-center p-2 bg-neutral-800/50 hover:bg-neutral-700/50 rounded transition-colors text-xs border border-neutral-700/30"
|
||||||
|
>
|
||||||
|
<BoomBox className="w-3 h-3 mr-2 text-neutral-400 flex-shrink-0" />
|
||||||
|
<span className="text-neutral-200 truncate">
|
||||||
|
{displayName}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{node.mapReport?.region !== undefined && (
|
{node.mapReport?.region !== undefined && (
|
||||||
<KeyValuePair
|
<KeyValuePair
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import React, { ReactNode } from "react";
|
|||||||
import { Packet } from "../../lib/types";
|
import { Packet } from "../../lib/types";
|
||||||
import { cn } from "@/lib/cn";
|
import { cn } from "@/lib/cn";
|
||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
|
import { useAppSelector } from "../../hooks";
|
||||||
|
import { getNodeDisplayName, getGatewayDisplayName } from "../../utils/formatters";
|
||||||
|
|
||||||
interface PacketCardProps {
|
interface PacketCardProps {
|
||||||
packet: Packet;
|
packet: Packet;
|
||||||
@@ -19,6 +21,18 @@ export const PacketCard: React.FC<PacketCardProps> = ({
|
|||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const { data } = packet;
|
const { data } = packet;
|
||||||
|
const { nodes } = useAppSelector((state) => state.aggregator);
|
||||||
|
|
||||||
|
// Get node data for sender and gateway
|
||||||
|
const senderNode = data.from ? nodes[data.from] : undefined;
|
||||||
|
const gatewayNode = data.gatewayId && data.gatewayId.startsWith('!')
|
||||||
|
? nodes[parseInt(data.gatewayId.substring(1), 16)]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// Check if gateway is the same as sender
|
||||||
|
const isGatewaySelf = data.from && data.gatewayId && data.gatewayId.startsWith('!')
|
||||||
|
? data.from === parseInt(data.gatewayId.substring(1), 16)
|
||||||
|
: false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl effect-inset rounded-lg border border-neutral-950/60 bg-neutral-800 overflow-hidden">
|
<div className="max-w-4xl effect-inset rounded-lg border border-neutral-950/60 bg-neutral-800 overflow-hidden">
|
||||||
@@ -43,7 +57,7 @@ export const PacketCard: React.FC<PacketCardProps> = ({
|
|||||||
params={{ nodeId: data.from.toString(16).toLowerCase() }}
|
params={{ nodeId: data.from.toString(16).toLowerCase() }}
|
||||||
className="font-semibold text-neutral-200 tracking-wide hover:text-blue-400 transition-colors"
|
className="font-semibold text-neutral-200 tracking-wide hover:text-blue-400 transition-colors"
|
||||||
>
|
>
|
||||||
!{data.from.toString(16).toLowerCase()}
|
{getNodeDisplayName(data.from, senderNode)}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<span className="font-semibold text-neutral-200 tracking-wide">Unknown</span>
|
<span className="font-semibold text-neutral-200 tracking-wide">Unknown</span>
|
||||||
@@ -57,13 +71,15 @@ export const PacketCard: React.FC<PacketCardProps> = ({
|
|||||||
{data.gatewayId && (
|
{data.gatewayId && (
|
||||||
<>
|
<>
|
||||||
<span className="text-neutral-500">via</span>
|
<span className="text-neutral-500">via</span>
|
||||||
{data.gatewayId.startsWith('!') ? (
|
{isGatewaySelf ? (
|
||||||
|
<span className="text-neutral-400">self</span>
|
||||||
|
) : data.gatewayId.startsWith('!') ? (
|
||||||
<Link
|
<Link
|
||||||
to="/node/$nodeId"
|
to="/node/$nodeId"
|
||||||
params={{ nodeId: data.gatewayId.substring(1) }}
|
params={{ nodeId: data.gatewayId.substring(1) }}
|
||||||
className="text-neutral-400 hover:text-blue-400 transition-colors"
|
className="text-neutral-400 hover:text-blue-400 transition-colors"
|
||||||
>
|
>
|
||||||
{data.gatewayId}
|
{getGatewayDisplayName(data.gatewayId, gatewayNode)}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-neutral-400">{data.gatewayId}</span>
|
<span className="text-neutral-400">{data.gatewayId}</span>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { RegionCode, ModemPreset } from "../lib/types";
|
import { RegionCode, ModemPreset } from "../lib/types";
|
||||||
|
import { NodeData } from "../store/slices/aggregatorSlice";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format uptime into a human-readable string
|
* Format uptime into a human-readable string
|
||||||
@@ -70,4 +71,42 @@ export const getModemPresetName = (
|
|||||||
|
|
||||||
// Get the name from the map, or return unknown with the value
|
// Get the name from the map, or return unknown with the value
|
||||||
return presetNames[preset] || `Unknown (${preset})`;
|
return presetNames[preset] || `Unknown (${preset})`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the display name for a node, preferring shortName over longName, with fallback to hex ID
|
||||||
|
*/
|
||||||
|
export const getNodeDisplayName = (
|
||||||
|
nodeId: number,
|
||||||
|
nodeData?: NodeData
|
||||||
|
): string => {
|
||||||
|
if (nodeData?.shortName) {
|
||||||
|
return nodeData.shortName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeData?.longName) {
|
||||||
|
return nodeData.longName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to hex ID format
|
||||||
|
return `!${nodeId.toString(16).toLowerCase()}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the display name for a gateway ID, with optional node data lookup
|
||||||
|
*/
|
||||||
|
export const getGatewayDisplayName = (
|
||||||
|
gatewayId: string,
|
||||||
|
nodeData?: NodeData
|
||||||
|
): string => {
|
||||||
|
if (nodeData?.shortName) {
|
||||||
|
return nodeData.shortName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeData?.longName) {
|
||||||
|
return nodeData.longName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the gateway ID as-is (already in !hex format)
|
||||||
|
return gatewayId;
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user