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 { 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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user