Tweak gateway visualization and card headers

This commit is contained in:
Daniel Pupius
2025-07-03 12:38:23 -07:00
parent f8c0e0d591
commit 88dd1fc663
3 changed files with 134 additions and 31 deletions

View File

@@ -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<NodeDetailProps> = ({ 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<NodeDetailProps> = ({ 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<NodeDetailProps> = ({ 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<NodeDetailProps> = ({ nodeId }) => {
</h1>
<div className="text-sm text-neutral-400 flex items-center">
<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>
{statusText} - last seen {lastSeenText}
</div>
</div>
<div className={cn(
"text-sm bg-neutral-900/50 px-3 py-1.5 rounded font-mono effect-inset tracking-wider",
activityColors.textClass
)}>
<div
className={cn(
"text-sm bg-neutral-900/50 px-3 py-1.5 rounded font-mono effect-inset tracking-wider",
activityColors.textClass
)}
>
!{nodeId.toString(16)}
</div>
</div>
@@ -333,23 +344,60 @@ export const NodeDetail: React.FC<NodeDetailProps> = ({ nodeId }) => {
{/* Show MapReport-specific information for gateways */}
{node.isGateway && (
<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 ">
<span className="flex items-center">
<Network className="w-4 h-4 mr-1.5" />
Gateway Node
</span>
{node.observedNodeCount !== undefined && (
<div className=" mb-2 p-2 rounded effect-inset bg-neutral-700/50 ">
<div className="flex justify-between items-center">
<span className="flex items-center">
<Users className="w-4 h-4 mr-1.5" />
{node.observedNodeCount}{" "}
{node.observedNodeCount === 1 ? "node" : "nodes"}
<Network className="w-4 h-4 mr-1.5" />
Gateway Node
</span>
)}
{node.mapReport?.numOnlineLocalNodes !== undefined && (
<span className="text-xs flex items-center font-mono opacity-80">
{node.mapReport.numOnlineLocalNodes} online local nodes
</span>
)}
{node.observedNodeCount !== undefined && (
<span className="flex items-center">
<Users className="w-4 h-4 mr-1.5" />
{node.observedNodeCount}{" "}
{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>
{node.mapReport?.region !== undefined && (
<KeyValuePair

View File

@@ -2,6 +2,8 @@ import React, { ReactNode } from "react";
import { Packet } from "../../lib/types";
import { cn } from "@/lib/cn";
import { Link } from "@tanstack/react-router";
import { useAppSelector } from "../../hooks";
import { getNodeDisplayName, getGatewayDisplayName } from "../../utils/formatters";
interface PacketCardProps {
packet: Packet;
@@ -19,6 +21,18 @@ export const PacketCard: React.FC<PacketCardProps> = ({
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 (
<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() }}
className="font-semibold text-neutral-200 tracking-wide hover:text-blue-400 transition-colors"
>
!{data.from.toString(16).toLowerCase()}
{getNodeDisplayName(data.from, senderNode)}
</Link>
) : (
<span className="font-semibold text-neutral-200 tracking-wide">Unknown</span>
@@ -57,13 +71,15 @@ export const PacketCard: React.FC<PacketCardProps> = ({
{data.gatewayId && (
<>
<span className="text-neutral-500">via</span>
{data.gatewayId.startsWith('!') ? (
{isGatewaySelf ? (
<span className="text-neutral-400">self</span>
) : data.gatewayId.startsWith('!') ? (
<Link
to="/node/$nodeId"
params={{ nodeId: data.gatewayId.substring(1) }}
className="text-neutral-400 hover:text-blue-400 transition-colors"
>
{data.gatewayId}
{getGatewayDisplayName(data.gatewayId, gatewayNode)}
</Link>
) : (
<span className="text-neutral-400">{data.gatewayId}</span>

View File

@@ -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;
};