From 88dd1fc6639593fe019c9f0c35b4be5003440143 Mon Sep 17 00:00:00 2001 From: Daniel Pupius Date: Thu, 3 Jul 2025 12:38:23 -0700 Subject: [PATCH] Tweak gateway visualization and card headers --- web/src/components/dashboard/NodeDetail.tsx | 104 ++++++++++++++------ web/src/components/packets/PacketCard.tsx | 22 ++++- web/src/utils/formatters.ts | 39 ++++++++ 3 files changed, 134 insertions(+), 31 deletions(-) diff --git a/web/src/components/dashboard/NodeDetail.tsx b/web/src/components/dashboard/NodeDetail.tsx index 5772a23..90d89ab 100644 --- a/web/src/components/dashboard/NodeDetail.tsx +++ b/web/src/components/dashboard/NodeDetail.tsx @@ -2,7 +2,12 @@ import React, { useEffect } from "react"; import { useNavigate, Link } from "@tanstack/react-router"; import { useAppSelector, useAppDispatch } from "../../hooks"; 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 { ArrowLeft, @@ -42,6 +47,7 @@ import { formatUptime, getRegionName, getModemPresetName, + getNodeDisplayName, } from "../../utils/formatters"; // Format role string for display @@ -66,11 +72,11 @@ const copyToClipboard = async (text: string) => { await navigator.clipboard.writeText(text); } catch { // Fallback for older browsers - const textArea = document.createElement('textarea'); + const textArea = document.createElement("textarea"); textArea.value = text; document.body.appendChild(textArea); textArea.select(); - document.execCommand('copy'); + document.execCommand("copy"); document.body.removeChild(textArea); } }; @@ -88,13 +94,13 @@ export const NodeDetail: React.FC = ({ nodeId }) => { // 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]; - + // First try to get the node directly from nodes collection let node = nodes[nodeId]; - + // If node exists but doesn't have isGateway set, check if it should be a gateway if (node && !node.isGateway && gateway) { // Update the node with gateway info @@ -105,7 +111,7 @@ export const NodeDetail: React.FC = ({ nodeId }) => { observedNodeCount: gateway.observedNodes.length, }; } - + // If node not found in nodes collection, create a synthetic node from gateway data if (!node && gateway) { // Create a synthetic node from the gateway data @@ -167,7 +173,7 @@ export const NodeDetail: React.FC = ({ nodeId }) => { // Calculate how recently node was active const secondsAgo = Math.floor(Date.now() / 1000) - node.lastHeard; - + // Use activity helpers const activityLevel = getActivityLevel(node.lastHeard, node.isGateway); const activityColors = getNodeColors(activityLevel, node.isGateway); @@ -226,15 +232,20 @@ export const NodeDetail: React.FC = ({ nodeId }) => {
{statusText} - last seen {lastSeenText}
-
+
!{nodeId.toString(16)}
@@ -333,23 +344,60 @@ export const NodeDetail: React.FC = ({ nodeId }) => { {/* Show MapReport-specific information for gateways */} {node.isGateway && (
-
- - - Gateway Node - - {node.observedNodeCount !== undefined && ( +
+
- - {node.observedNodeCount}{" "} - {node.observedNodeCount === 1 ? "node" : "nodes"} + + Gateway Node - )} - {node.mapReport?.numOnlineLocalNodes !== undefined && ( - - {node.mapReport.numOnlineLocalNodes} online local nodes - - )} + {node.observedNodeCount !== undefined && ( + + + {node.observedNodeCount}{" "} + {node.observedNodeCount === 1 ? "node" : "nodes"} + + )} + {node.mapReport?.numOnlineLocalNodes !== undefined && ( + + {node.mapReport.numOnlineLocalNodes} online local nodes + + )} +
+ {/* Observed Nodes Grid - integrated into Gateway Node section */} + {gateway?.observedNodes && + gateway.observedNodes.length > 0 && ( +
+
+ Recently observed nodes: +
+
+ {gateway.observedNodes.map((observedNodeId) => { + const observedNode = nodes[observedNodeId]; + const displayName = getNodeDisplayName( + observedNodeId, + observedNode + ); + return ( + + + + {displayName} + + + ); + })} +
+
+ )}
{node.mapReport?.region !== undefined && ( = ({ children, }) => { 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 (
@@ -43,7 +57,7 @@ export const PacketCard: React.FC = ({ params={{ nodeId: data.from.toString(16).toLowerCase() }} className="font-semibold text-neutral-200 tracking-wide hover:text-blue-400 transition-colors" > - !{data.from.toString(16).toLowerCase()} + {getNodeDisplayName(data.from, senderNode)} ) : ( Unknown @@ -57,13 +71,15 @@ export const PacketCard: React.FC = ({ {data.gatewayId && ( <> via - {data.gatewayId.startsWith('!') ? ( + {isGatewaySelf ? ( + self + ) : data.gatewayId.startsWith('!') ? ( - {data.gatewayId} + {getGatewayDisplayName(data.gatewayId, gatewayNode)} ) : ( {data.gatewayId} diff --git a/web/src/utils/formatters.ts b/web/src/utils/formatters.ts index fe33c14..e033344 100644 --- a/web/src/utils/formatters.ts +++ b/web/src/utils/formatters.ts @@ -1,4 +1,5 @@ import { RegionCode, ModemPreset } from "../lib/types"; +import { NodeData } from "../store/slices/aggregatorSlice"; /** * 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 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; }; \ No newline at end of file