import { useEffect, useMemo, useState } from 'react'; import { ChevronLeft, ChevronRight } from 'lucide-react'; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer, Cell, } from 'recharts'; import { RawPacketList } from './RawPacketList'; import { RawPacketInspectorDialog } from './RawPacketDetailModal'; import { Button } from './ui/button'; import type { Channel, Contact, RawPacket } from '../types'; import { RAW_PACKET_STATS_WINDOWS, buildRawPacketStatsSnapshot, type NeighborStat, type PacketTimelineBin, type RankedPacketStat, type RawPacketStatsSessionState, type RawPacketStatsWindow, } from '../utils/rawPacketStats'; import { getContactDisplayName } from '../utils/pubkey'; import { cn } from '@/lib/utils'; interface RawPacketFeedViewProps { packets: RawPacket[]; rawPacketStatsSession: RawPacketStatsSessionState; contacts: Contact[]; channels: Channel[]; } const TOOLTIP_STYLE = { contentStyle: { backgroundColor: 'hsl(var(--popover))', border: '1px solid hsl(var(--border))', borderRadius: '6px', fontSize: '11px', color: 'hsl(var(--popover-foreground))', }, itemStyle: { color: 'hsl(var(--popover-foreground))' }, labelStyle: { color: 'hsl(var(--muted-foreground))' }, } as const; const WINDOW_LABELS: Record = { '1m': '1 min', '5m': '5 min', '10m': '10 min', '30m': '30 min', session: 'Session', }; const TIMELINE_FILL_COLORS = ['#0ea5e9', '#10b981', '#f59e0b', '#f43f5e', '#8b5cf6']; function formatTimestamp(timestampMs: number): string { return new Date(timestampMs).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', }); } function formatDuration(seconds: number): string { if (seconds < 60) { return `${Math.max(1, Math.round(seconds))} sec`; } if (seconds < 3600) { const minutes = Math.floor(seconds / 60); const remainder = Math.round(seconds % 60); return remainder > 0 ? `${minutes}m ${remainder}s` : `${minutes}m`; } const hours = Math.floor(seconds / 3600); const minutes = Math.round((seconds % 3600) / 60); return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`; } function formatRate(value: number): string { if (value >= 100) return value.toFixed(0); if (value >= 10) return value.toFixed(1); return value.toFixed(2); } function formatPercent(value: number): string { return `${Math.round(value * 100)}%`; } function formatRssi(value: number | null): string { return value === null ? '-' : `${Math.round(value)} dBm`; } function normalizeResolvableSourceKey(sourceKey: string): string { return sourceKey.startsWith('hash1:') ? sourceKey.slice(6) : sourceKey; } function resolveContact(sourceKey: string | null, contacts: Contact[]): Contact | null { if (!sourceKey || sourceKey.startsWith('name:')) { return null; } const normalizedSourceKey = normalizeResolvableSourceKey(sourceKey).toLowerCase(); const matches = contacts.filter((contact) => contact.public_key.toLowerCase().startsWith(normalizedSourceKey) ); if (matches.length !== 1) { return null; } return matches[0]; } function resolveContactLabel(sourceKey: string | null, contacts: Contact[]): string | null { const contact = resolveContact(sourceKey, contacts); if (!contact) { return null; } return getContactDisplayName(contact.name, contact.public_key, contact.last_advert); } function resolveNeighbor(item: NeighborStat, contacts: Contact[]): NeighborStat { return { ...item, label: resolveContactLabel(item.key, contacts) ?? item.label, }; } function mergeResolvedNeighbors(items: NeighborStat[], contacts: Contact[]): NeighborStat[] { const merged = new Map(); for (const item of items) { const contact = resolveContact(item.key, contacts); const canonicalKey = contact?.public_key ?? item.key; const resolvedLabel = contact != null ? getContactDisplayName(contact.name, contact.public_key, contact.last_advert) : item.label; const existing = merged.get(canonicalKey); if (!existing) { merged.set(canonicalKey, { ...item, key: canonicalKey, label: resolvedLabel, }); continue; } existing.count += item.count; existing.lastSeen = Math.max(existing.lastSeen, item.lastSeen); existing.bestRssi = existing.bestRssi === null ? item.bestRssi : item.bestRssi === null ? existing.bestRssi : Math.max(existing.bestRssi, item.bestRssi); existing.label = resolvedLabel; } return Array.from(merged.values()); } function isNeighborIdentityResolvable(item: NeighborStat, contacts: Contact[]): boolean { if (item.key.startsWith('name:')) { return true; } return resolveContact(item.key, contacts) !== null; } function formatStrongestNeighborDetail( stats: ReturnType, contacts: Contact[] ): string | undefined { const strongestNeighbor = stats.strongestNeighbors[0]; if (!strongestNeighbor || strongestNeighbor.bestRssi === null) { return undefined; } const resolvedNeighbor = resolveNeighbor(strongestNeighbor, contacts); return `${formatRssi(resolvedNeighbor.bestRssi)} best heard`; } function getCoverageMessage( stats: ReturnType, session: RawPacketStatsSessionState ): { tone: 'default' | 'warning'; message: string } { if (session.trimmedObservationCount > 0 && stats.window === 'session') { return { tone: 'warning', message: `Detailed session history was trimmed after ${session.totalObservedPackets.toLocaleString()} observations.`, }; } if (!stats.windowFullyCovered) { return { tone: 'warning', message: `This window is only covered for ${formatDuration(stats.coverageSeconds)} of frontend-collected history.`, }; } return { tone: 'default', message: `Tracking ${session.observations.length.toLocaleString()} detailed observations from this browser session.`, }; } function StatTile({ label, value, detail }: { label: string; value: string; detail?: string }) { return (
{label}
{value}
{detail ?
{detail}
: null}
); } function RankedBars({ title, items, emptyLabel, formatter, }: { title: string; items: RankedPacketStat[]; emptyLabel: string; formatter?: (item: RankedPacketStat) => string; }) { const data = items.map((item) => ({ name: item.label, value: item.count, detail: formatter ? formatter(item) : `${item.count.toLocaleString()} · ${formatPercent(item.share)}`, })); return (

{title}

{items.length === 0 ? (

{emptyLabel}

) : (
[props.payload.detail, null]} /> {data.map((_, i) => ( ))}
)}
); } function NeighborList({ title, items, emptyLabel, mode, contacts, }: { title: string; items: NeighborStat[]; emptyLabel: string; mode: 'heard' | 'signal' | 'recent'; contacts: Contact[]; }) { const mergedItems = mergeResolvedNeighbors(items, contacts); const sortedItems = [...mergedItems].sort((a, b) => { if (mode === 'heard') { return b.count - a.count || b.lastSeen - a.lastSeen || a.label.localeCompare(b.label); } if (mode === 'signal') { return ( (b.bestRssi ?? Number.NEGATIVE_INFINITY) - (a.bestRssi ?? Number.NEGATIVE_INFINITY) || b.count - a.count || a.label.localeCompare(b.label) ); } return b.lastSeen - a.lastSeen || b.count - a.count || a.label.localeCompare(b.label); }); return (

{title}

{sortedItems.length === 0 ? (

{emptyLabel}

) : (
{sortedItems.map((item) => (
{item.label}
{mode === 'heard' ? `${item.count.toLocaleString()} packets` : mode === 'signal' ? `${formatRssi(item.bestRssi)} best` : `Last seen ${new Date(item.lastSeen * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`}
{!isNeighborIdentityResolvable(item, contacts) ? (
Identity not resolvable
) : null}
{mode !== 'signal' ? (
{mode === 'recent' ? formatRssi(item.bestRssi) : formatRssi(item.bestRssi)}
) : null}
))}
)}
); } function TimelineChart({ bins }: { bins: PacketTimelineBin[] }) { const typeOrder = Array.from(new Set(bins.flatMap((bin) => Object.keys(bin.countsByType)))).slice( 0, TIMELINE_FILL_COLORS.length ); const data = bins.map((bin) => { const entry: Record = { label: bin.label }; for (const type of typeOrder) { entry[type] = bin.countsByType[type] ?? 0; } return entry; }); return (

Traffic Timeline

{typeOrder.map((type, i) => ( {type} ))}
{typeOrder.map((type, i) => ( ))}
); } export function RawPacketFeedView({ packets, rawPacketStatsSession, contacts, channels, }: RawPacketFeedViewProps) { const [statsOpen, setStatsOpen] = useState(() => typeof window !== 'undefined' && typeof window.matchMedia === 'function' ? window.matchMedia('(min-width: 768px)').matches : false ); const [selectedWindow, setSelectedWindow] = useState('10m'); const [nowSec, setNowSec] = useState(() => Math.floor(Date.now() / 1000)); const [selectedPacket, setSelectedPacket] = useState(null); const [analyzeModalOpen, setAnalyzeModalOpen] = useState(false); useEffect(() => { const interval = window.setInterval(() => { setNowSec(Math.floor(Date.now() / 1000)); }, 30000); return () => window.clearInterval(interval); }, []); useEffect(() => { setNowSec(Math.floor(Date.now() / 1000)); }, [packets, rawPacketStatsSession]); const stats = useMemo( () => buildRawPacketStatsSnapshot(rawPacketStatsSession, selectedWindow, nowSec), [nowSec, rawPacketStatsSession, selectedWindow] ); const coverageMessage = getCoverageMessage(stats, rawPacketStatsSession); const strongestNeighbor = useMemo(() => { const topNeighbor = stats.strongestNeighbors[0]; return topNeighbor ? resolveNeighbor(topNeighbor, contacts) : null; }, [contacts, stats]); const strongestNeighborDetail = useMemo( () => formatStrongestNeighborDetail(stats, contacts), [contacts, stats] ); const strongestNeighbors = useMemo( () => stats.strongestNeighbors.map((item) => resolveNeighbor(item, contacts)), [contacts, stats.strongestNeighbors] ); const mostActiveNeighbors = useMemo( () => stats.mostActiveNeighbors.map((item) => resolveNeighbor(item, contacts)), [contacts, stats.mostActiveNeighbors] ); const newestNeighbors = useMemo( () => stats.newestNeighbors.map((item) => resolveNeighbor(item, contacts)), [contacts, stats.newestNeighbors] ); return ( <>

Raw Packet Feed

Collecting stats since {formatTimestamp(rawPacketStatsSession.sessionStartedAt)}

!isOpen && setSelectedPacket(null)} channels={channels} source={ selectedPacket ? { kind: 'packet', packet: selectedPacket } : { kind: 'loading', message: 'Loading packet...' } } title="Packet Details" description="Detailed byte and field breakdown for the selected raw packet." /> ); }