diff --git a/frontend/src/components/RawPacketFeedView.tsx b/frontend/src/components/RawPacketFeedView.tsx index f51be68..134343d 100644 --- a/frontend/src/components/RawPacketFeedView.tsx +++ b/frontend/src/components/RawPacketFeedView.tsx @@ -31,7 +31,29 @@ import { createDecoderOptions } from '../utils/rawPacketInspector'; import { getContactDisplayName } from '../utils/pubkey'; import { cn } from '@/lib/utils'; +const TIMELINE_FILL_COLORS = ['#0ea5e9', '#10b981', '#f59e0b', '#f43f5e', '#8b5cf6']; + +/** + * Build a stable name→color mapping so the same type always gets the same + * color regardless of sort order or appearance order. + */ +function buildColorMap(names: readonly string[]): Map { + const map = new Map(); + for (let i = 0; i < names.length; i++) { + map.set(names[i], TIMELINE_FILL_COLORS[i % TIMELINE_FILL_COLORS.length]); + } + return map; +} + +function colorForIndex(index: number, colorMap?: Map, name?: string): string { + if (colorMap && name && colorMap.has(name)) { + return colorMap.get(name)!; + } + return TIMELINE_FILL_COLORS[index % TIMELINE_FILL_COLORS.length]; +} + const KNOWN_PAYLOAD_TYPE_SET = new Set(KNOWN_PAYLOAD_TYPES); +const PAYLOAD_TYPE_COLOR_MAP = buildColorMap(KNOWN_PAYLOAD_TYPES); function getPacketTypeName( packet: RawPacket, @@ -74,8 +96,6 @@ const WINDOW_LABELS: Record = { session: 'Session', }; -const TIMELINE_FILL_COLORS = ['#0ea5e9', '#10b981', '#f59e0b', '#f43f5e', '#8b5cf6']; - function formatTimestamp(timestampMs: number): string { return new Date(timestampMs).toLocaleString([], { month: 'short', @@ -245,11 +265,13 @@ function RankedBars({ items, emptyLabel, formatter, + colorMap, }: { title: string; items: RankedPacketStat[]; emptyLabel: string; formatter?: (item: RankedPacketStat) => string; + colorMap?: Map; }) { const data = items.map((item) => ({ name: item.label, @@ -289,8 +311,8 @@ function RankedBars({ formatter={(_v: any, _n: any, props: any) => [props.payload.detail, null]} /> - {data.map((_, i) => ( - + {data.map((entry, i) => ( + ))} @@ -367,7 +389,13 @@ function NeighborList({ ); } -function TimelineChart({ bins }: { bins: PacketTimelineBin[] }) { +function TimelineChart({ + bins, + colorMap, +}: { + bins: PacketTimelineBin[]; + colorMap: Map; +}) { const typeOrder = Array.from(new Set(bins.flatMap((bin) => Object.keys(bin.countsByType)))).slice( 0, TIMELINE_FILL_COLORS.length @@ -386,11 +414,11 @@ function TimelineChart({ bins }: { bins: PacketTimelineBin[] }) {

Traffic Timeline

- {typeOrder.map((type, i) => ( + {typeOrder.map((type) => ( {type} @@ -422,7 +450,7 @@ function TimelineChart({ bins }: { bins: PacketTimelineBin[] }) { key={type} dataKey={type} stackId="packets" - fill={TIMELINE_FILL_COLORS[i]} + fill={colorMap.get(type) ?? TIMELINE_FILL_COLORS[0]} radius={i === typeOrder.length - 1 ? [2, 2, 0, 0] : undefined} /> ))} @@ -747,7 +775,7 @@ export function RawPacketFeedView({
- +
@@ -755,6 +783,7 @@ export function RawPacketFeedView({ title="Packet Types" items={stats.payloadBreakdown} emptyLabel="No packets in this window yet." + colorMap={PAYLOAD_TYPE_COLOR_MAP} />