From abf3b4673ae10e53825c6be413c8df8ade38a01d Mon Sep 17 00:00:00 2001 From: Javier Ribal del Rio Date: Tue, 6 Oct 2026 08:31:58 +0200 Subject: [PATCH 1/4] fix(adj-view): throughput calculator period was uncorrrectly cached --- frontend/adj-view/src/adj/v2/AdjViewerTabs.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/adj-view/src/adj/v2/AdjViewerTabs.tsx b/frontend/adj-view/src/adj/v2/AdjViewerTabs.tsx index 0ac811b90..630d95c99 100644 --- a/frontend/adj-view/src/adj/v2/AdjViewerTabs.tsx +++ b/frontend/adj-view/src/adj/v2/AdjViewerTabs.tsx @@ -650,6 +650,13 @@ function GeneralTab({ adjData }: { adjData: AdjArchiveV2 }) { export const AdjViewerTabs = ({ adjData }: { adjData: AdjArchiveV2 }) => { const boards = useMemo(() => extractBoards(adjData), [adjData]); + const [archive, setArchive] = useState({ data: adjData, revision: 0 }); + + // Reset the scenario for every newly loaded archive, even when board names + // are unchanged. Keep the active tab so users can compare commits in place. + if (archive.data !== adjData) { + setArchive({ data: adjData, revision: archive.revision + 1 }); + } // Lifted state for cross-tab navigation const [activeTab, setActiveTab] = useState("boards"); @@ -729,8 +736,7 @@ export const AdjViewerTabs = ({ adjData }: { adjData: AdjArchiveV2 }) => { {/* No overflow-hidden here: it would trap ThroughputTab's sticky panel. */} - {/* key resets the TCP-connected defaults when a different board set loads */} - b.name).join(",")} boards={boards} /> + From 9e3fbffe3dff86833aaeba31b2a5e2bb0f2b5980 Mon Sep 17 00:00:00 2001 From: Javier Ribal del Rio Date: Tue, 6 Oct 2026 08:55:19 +0200 Subject: [PATCH 2/4] feat(adj-view): highlight packets when click --- .../adj-view/src/adj/v2/ThroughputTab.tsx | 79 +++++++++++++++++-- 1 file changed, 71 insertions(+), 8 deletions(-) diff --git a/frontend/adj-view/src/adj/v2/ThroughputTab.tsx b/frontend/adj-view/src/adj/v2/ThroughputTab.tsx index a07766ae7..b784ca54f 100644 --- a/frontend/adj-view/src/adj/v2/ThroughputTab.tsx +++ b/frontend/adj-view/src/adj/v2/ThroughputTab.tsx @@ -333,10 +333,14 @@ function FlowTable({ title, flows, onPeriodChange, + selectedKey, + onSelectFlow, }: { title: string; flows: TrafficFlow[]; onPeriodChange?: OnPeriodChange; + selectedKey?: string; + onSelectFlow?: (flow: TrafficFlow | null) => void; }) { const [open, setOpen] = useState(null); const directions = (["up", "down"] as const).filter((d) => flows.some((f) => f.direction === d)); @@ -347,6 +351,7 @@ function FlowTable({

{title}

{flows.length} {flows.length === 1 ? "flow" : "flows"}, click one to see its calculation + {onSelectFlow && " and highlight its bandwidth"} @@ -364,20 +369,31 @@ function FlowTable({ {flows.map((f) => { const isOpen = open === f.key; + const isSelected = selectedKey === f.key; + const toggle = () => { + setOpen(isOpen ? null : f.key); + onSelectFlow?.(isSelected ? null : f); + }; return ( setOpen(isOpen ? null : f.key)} - className={cn("hover:bg-muted/40 cursor-pointer border-b last:border-0", isOpen && "bg-muted/40")} + onClick={toggle} + className={cn("hover:bg-muted/40 cursor-pointer border-b last:border-0", isOpen && "bg-muted/40", isSelected && "bg-primary/10")} > {onPeriodChange ? ( @@ -428,6 +444,7 @@ interface Segment { color: string; udp: number; keepAlive: number; + highlight?: { offset: number; bps: number; label: string }; } function StackedBar({ title, segments, capacityBps }: { title: string; segments: Segment[]; capacityBps: number }) { @@ -436,6 +453,7 @@ function StackedBar({ title, segments, capacityBps }: { title: string; segments: // Over capacity, scale to the total so every board still shows. const scale = Math.max(capacityBps, total); const visible = segments.filter((s) => s.udp + s.keepAlive > 0); + const highlighted = segments.find((s) => s.highlight)?.highlight; return (
@@ -453,8 +471,24 @@ function StackedBar({ title, segments, capacityBps }: { title: string; segments:
- {s.udp > 0 &&
} - {s.keepAlive > 0 &&
} + {s.udp > 0 && ( +
+
+ {s.highlight && ( +
+ )} +
+ )} + {s.keepAlive > 0 &&
}
@@ -470,6 +504,11 @@ function StackedBar({ title, segments, capacityBps }: { title: string; segments: })} {!over &&
}
+ {highlighted && ( +

+ {highlighted.label}: {formatBitrate(highlighted.bps)} · {pctOf(highlighted.bps, capacityBps)} of the link +

+ )}
{over ? ( @@ -592,6 +631,7 @@ export function ThroughputTab({ boards }: { boards: BoardMeta[] }) { const [boardMs, setBoardMs] = useState("50"); const [expanded, setExpanded] = useState(null); const [periodOverrides, setPeriodOverrides] = useState({}); + const [selectedPacket, setSelectedPacket] = useState<{ board: string; key: string } | null>(null); const rows = useMemo( () => @@ -646,9 +686,19 @@ export function ThroughputTab({ boards }: { boards: BoardMeta[] }) { const other: Segment = { key: "__other", label: "Other boards", color: seriesColor(SERIES_SLOTS), udp: 0, keepAlive: 0 }; rows.forEach((r, i) => { const t = totals.get(r.board)!; + const packetIndex = selectedPacket?.board === r.board + ? r.packets.findIndex((p) => p.key === selectedPacket.key) + : -1; + const packet = r.packets[packetIndex]; + const highlight = packet && udp(t) > 0 ? { + offset: r.packets.slice(0, packetIndex).reduce((s, p) => s + p.wireBps, 0), + bps: packet.wireBps, + label: `${r.board} / ${packet.name} #${packet.id}`, + } : undefined; if (i < SERIES_SLOTS) { - segments.push({ key: r.board, label: r.board, color: seriesColor(i), udp: udp(t), keepAlive: keepAlive(t) }); + segments.push({ key: r.board, label: r.board, color: seriesColor(i), udp: udp(t), keepAlive: keepAlive(t), highlight }); } else { + if (highlight) other.highlight = { ...highlight, offset: other.udp + highlight.offset }; other.udp += udp(t); other.keepAlive += keepAlive(t); } @@ -811,6 +861,11 @@ export function ThroughputTab({ boards }: { boards: BoardMeta[] }) { capacityBps={capacityBps} /> )} + {selectedPacket && options.udp && ( + + )}
{rows.map((r, i) => ( @@ -861,7 +916,7 @@ export function ThroughputTab({ boards }: { boards: BoardMeta[] }) {
@@ -949,6 +1006,8 @@ function BoardDetail({ totals, capacityBps, onPeriodChange, + selectedKey, + onSelectFlow, }: { board: BoardThroughput; options: TrafficOptions; @@ -957,6 +1016,8 @@ function BoardDetail({ totals: BoardTotals; capacityBps: number | null; onPeriodChange: OnPeriodChange; + selectedKey?: string; + onSelectFlow: (flow: TrafficFlow | null) => void; }) { const note = (text: string) => (

{text}

@@ -987,6 +1048,8 @@ function BoardDetail({ title="UDP data packets" flows={[...board.packets].sort((a, b) => a.id! - b.id!)} onPeriodChange={onPeriodChange} + selectedKey={selectedKey} + onSelectFlow={onSelectFlow} /> ) : note("This board has no periodic UDP packets.")} From c7a237ce8cfecceecc00ea409be11fa018b4e51d Mon Sep 17 00:00:00 2001 From: Javier Ribal del Rio Date: Tue, 6 Oct 2026 09:00:38 +0200 Subject: [PATCH 3/4] feat(adj-view): add chart to each board --- .../adj-view/src/adj/v2/ThroughputTab.tsx | 125 ++++++++++++++++-- 1 file changed, 116 insertions(+), 9 deletions(-) diff --git a/frontend/adj-view/src/adj/v2/ThroughputTab.tsx b/frontend/adj-view/src/adj/v2/ThroughputTab.tsx index b784ca54f..2a9c746d0 100644 --- a/frontend/adj-view/src/adj/v2/ThroughputTab.tsx +++ b/frontend/adj-view/src/adj/v2/ThroughputTab.tsx @@ -447,6 +447,16 @@ interface Segment { highlight?: { offset: number; bps: number; label: string }; } +function packetHighlight(board: BoardThroughput, key?: string): Segment["highlight"] { + const index = board.packets.findIndex((p) => p.key === key); + const packet = board.packets[index]; + return packet ? { + offset: board.packets.slice(0, index).reduce((sum, p) => sum + p.wireBps, 0), + bps: packet.wireBps, + label: `${board.board} / ${packet.name} #${packet.id}`, + } : undefined; +} + function StackedBar({ title, segments, capacityBps }: { title: string; segments: Segment[]; capacityBps: number }) { const total = segments.reduce((s, x) => s + x.udp + x.keepAlive, 0); const over = total > capacityBps; @@ -526,6 +536,79 @@ function StackedBar({ title, segments, capacityBps }: { title: string; segments: ); } +// Each row is normalized to the board's traffic, rather than link capacity. +function PacketShareBar({ title, flows, colors, selectedKey, onSelectFlow }: { + title: string; + flows: TrafficFlow[]; + colors: ReadonlyMap; + selectedKey?: string; + onSelectFlow: (flow: TrafficFlow | null) => void; +}) { + const total = flows.reduce((sum, f) => sum + f.wireBps, 0); + const highlighted = flows.some((f) => f.key === selectedKey); + const label = (f: TrafficFlow) => `${f.name}${f.id == null ? "" : ` #${f.id}`}`; + return ( +
+
+

{title}

+ {formatBitrate(total)}{total > 0 && " · 100% of this traffic"} +
+ {total > 0 ? ( + <> +
+ {flows.filter((f) => f.wireBps > 0).map((f) => { + const selected = selectedKey === f.key; + const color = colors.get(f.key)!; + return ( + + +
+ {f.transport === "UDP" && ( +
+
+ + {label(f)} · {formatBitrate(f.wireBps)} · {pctOf(f.wireBps, total)} + +
+ ); + })} +
+
+ {flows.map((f) => ( + + + {f.transport === "UDP" ? ( + + ) : {label(f)} · {pctOf(f.wireBps, total)}} + + ))} +
+ + ) :

No traffic included in this direction.

} +
+ ); +} + // ─── scenario panel ────────────────────────────────────────────────────────── function Group({ title, children }: { title: string; children: ReactNode }) { @@ -686,15 +769,9 @@ export function ThroughputTab({ boards }: { boards: BoardMeta[] }) { const other: Segment = { key: "__other", label: "Other boards", color: seriesColor(SERIES_SLOTS), udp: 0, keepAlive: 0 }; rows.forEach((r, i) => { const t = totals.get(r.board)!; - const packetIndex = selectedPacket?.board === r.board - ? r.packets.findIndex((p) => p.key === selectedPacket.key) - : -1; - const packet = r.packets[packetIndex]; - const highlight = packet && udp(t) > 0 ? { - offset: r.packets.slice(0, packetIndex).reduce((s, p) => s + p.wireBps, 0), - bps: packet.wireBps, - label: `${r.board} / ${packet.name} #${packet.id}`, - } : undefined; + const highlight = selectedPacket?.board === r.board && udp(t) > 0 + ? packetHighlight(r, selectedPacket.key) + : undefined; if (i < SERIES_SLOTS) { segments.push({ key: r.board, label: r.board, color: seriesColor(i), udp: udp(t), keepAlive: keepAlive(t), highlight }); } else { @@ -968,6 +1045,7 @@ export function ThroughputTab({ boards }: { boards: BoardMeta[] }) { kaFlows={kaFlows} totals={t} capacityBps={capacityBps} + view={view} onPeriodChange={(f, o) => setPeriod(r.board, f, o)} selectedKey={selectedPacket?.board === r.board ? selectedPacket.key : undefined} onSelectFlow={(f) => setSelectedPacket(f ? { board: r.board, key: f.key } : null)} @@ -1005,6 +1083,7 @@ function BoardDetail({ kaFlows, totals, capacityBps, + view, onPeriodChange, selectedKey, onSelectFlow, @@ -1015,6 +1094,7 @@ function BoardDetail({ kaFlows: TrafficFlow[]; totals: BoardTotals; capacityBps: number | null; + view: View; onPeriodChange: OnPeriodChange; selectedKey?: string; onSelectFlow: (flow: TrafficFlow | null) => void; @@ -1022,6 +1102,14 @@ function BoardDetail({ const note = (text: string) => (

{text}

); + const udpFlows = [...board.packets].sort((a, b) => a.id! - b.id!); + const flows = [ + ...(options.udp ? udpFlows : []), + ...(options.keepAlive && connected ? kaFlows : []), + ]; + // Keep packet colors stable when traffic options are toggled. + const colors = new Map(udpFlows.map((f, i) => [f.key, seriesColor(i % SERIES_SLOTS)])); + for (const f of kaFlows) colors.set(f.key, "var(--series-other)"); return (
@@ -1080,6 +1168,25 @@ function BoardDetail({ ))}
)} +
+

Packet share of {board.board} traffic. Each row represents 100% of its total.

+ f.direction === "up") : flows} + colors={colors} + selectedKey={selectedKey} + onSelectFlow={onSelectFlow} + /> + {view === "direction" && ( + f.direction === "down")} + colors={colors} + selectedKey={selectedKey} + onSelectFlow={onSelectFlow} + /> + )} +
); } From 5021f3be5f7ab247c755a125854b09b89a4465db Mon Sep 17 00:00:00 2001 From: Javier Ribal del Rio Date: Tue, 6 Oct 2026 09:00:57 +0200 Subject: [PATCH 4/4] feat(adj-view): max option --- .../adj-view/src/adj/v2/ThroughputTab.tsx | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/frontend/adj-view/src/adj/v2/ThroughputTab.tsx b/frontend/adj-view/src/adj/v2/ThroughputTab.tsx index 2a9c746d0..08c6bb98b 100644 --- a/frontend/adj-view/src/adj/v2/ThroughputTab.tsx +++ b/frontend/adj-view/src/adj/v2/ThroughputTab.tsx @@ -710,6 +710,7 @@ export function ThroughputTab({ boards }: { boards: BoardMeta[] }) { const [options, setOptions] = useState(DEFAULT_TRAFFIC_OPTIONS); const [view, setView] = useState("direction"); const [capacityMbps, setCapacityMbps] = useState("100"); + const [maxCapacity, setMaxCapacity] = useState(false); const [backendMs, setBackendMs] = useState("50"); const [boardMs, setBoardMs] = useState("50"); const [expanded, setExpanded] = useState(null); @@ -742,7 +743,15 @@ export function ThroughputTab({ boards }: { boards: BoardMeta[] }) { ); const sum = (pick: (t: BoardTotals) => number) => rows.reduce((s, r) => s + pick(totals.get(r.board)!), 0); - const capacityBps = Number(capacityMbps) > 0 ? Number(capacityMbps) * 1e6 : null; + const maxCapacityBps = view === "direction" + ? Math.max(sum((t) => t.up), sum((t) => t.down)) + : sum((t) => t.up + t.down); + const capacityBps = maxCapacity + ? Math.max(maxCapacityBps, 1) + : Number(capacityMbps) > 0 ? Number(capacityMbps) * 1e6 : null; + const displayedCapacityMbps = maxCapacity + ? (maxCapacityBps / 1e6).toPrecision(4) + : capacityMbps; const modifiedCount = (board: string) => Object.keys(periodOverrides[board] ?? {}).length; const totalModified = Object.values(periodOverrides).reduce((s, m) => s + Object.keys(m).length, 0); @@ -806,16 +815,22 @@ export function ThroughputTab({ boards }: { boards: BoardMeta[] }) { - + { setCapacityMbps(value); setMaxCapacity(false); }} + />
{CAPACITY_PRESETS.map((p) => ( ))} +
+ {maxCapacity && ( +

+ Scales to the highest current chart total and updates with the scenario. +

+ )}
- + {DIRECTION_LABEL[f.direction]}