diff --git a/frontend/src/components/PacketVisualizer3D.tsx b/frontend/src/components/PacketVisualizer3D.tsx index 3ab2c56..f9de3a5 100644 --- a/frontend/src/components/PacketVisualizer3D.tsx +++ b/frontend/src/components/PacketVisualizer3D.tsx @@ -56,6 +56,7 @@ interface GraphNode extends SimulationNodeDatum3D { type: NodeType; isAmbiguous: boolean; lastActivity: number; + lastActivityReason?: string; lastSeen?: number | null; probableIdentity?: string | null; ambiguousNames?: string[]; @@ -110,6 +111,15 @@ function arraysEqual(a: string[], b: string[]): boolean { return true; } +function formatRelativeTime(timestamp: number): string { + const seconds = Math.floor((Date.now() - timestamp) / 1000); + if (seconds < 5) return 'just now'; + if (seconds < 60) return `${seconds}s ago`; + const minutes = Math.floor(seconds / 60); + const secs = seconds % 60; + return secs > 0 ? `${minutes}m ${secs}s ago` : `${minutes}m ago`; +} + // ============================================================================= // DATA LAYER HOOK (3D variant) // ============================================================================= @@ -725,6 +735,15 @@ function useVisualizerData3D({ const path = buildPath(parsed, packet, myPrefix); if (path.length < 2) continue; + // Tag each node with why it's considered active + const label = getPacketLabel(parsed.payloadType); + for (let i = 0; i < path.length; i++) { + const n = nodesRef.current.get(path[i]); + if (n && n.id !== 'self') { + n.lastActivityReason = i === 0 ? `${label} source` : `Relayed ${label}`; + } + } + for (let i = 0; i < path.length - 1; i++) { if (path[i] !== path[i + 1]) { addLink(path[i], path[i + 1]); @@ -1909,6 +1928,12 @@ export function PacketVisualizer3D({ {node.ambiguousNames.join(', ')} )} + {pinnedNodeId && node.type !== 'self' && ( +