diff --git a/src/features/packets/PacketRow.tsx b/src/features/packets/PacketRow.tsx
index 1c941b0..2fcba53 100644
--- a/src/features/packets/PacketRow.tsx
+++ b/src/features/packets/PacketRow.tsx
@@ -54,6 +54,8 @@ export function PacketRow({ packet, expanded, isFresh, onToggle }: PacketRowProp
+ {packet.summary &&
{packet.summary}
}
+
{packet.routeTypeName || "Unknown"}
diff --git a/src/features/packets/PacketTableHeader.tsx b/src/features/packets/PacketTableHeader.tsx
index 97ae3da..e84f7ec 100644
--- a/src/features/packets/PacketTableHeader.tsx
+++ b/src/features/packets/PacketTableHeader.tsx
@@ -14,7 +14,7 @@ export function PacketTableHeader() {
Obs
Hops
Hash Size
- Src → Dst
+ Summary / Src → Dst
IATA
Age
diff --git a/src/features/packets/PacketTableRow.tsx b/src/features/packets/PacketTableRow.tsx
index 95a430f..2033903 100644
--- a/src/features/packets/PacketTableRow.tsx
+++ b/src/features/packets/PacketTableRow.tsx
@@ -58,6 +58,7 @@ export function PacketTableRow({ packet, expanded, isFresh, onToggle }: PacketTa
{pathLength?.hopCount ?? na}
{pathLength?.hashSize ?? na}
+ {packet.summary && {packet.summary}}
diff --git a/src/features/packets/usePackets.ts b/src/features/packets/usePackets.ts
index ef453ee..3f03b55 100644
--- a/src/features/packets/usePackets.ts
+++ b/src/features/packets/usePackets.ts
@@ -163,6 +163,7 @@ export function usePackets(frozen: boolean = false, serverFilter: PacketServerFi
lastHeardAt: data.observation.heardAt,
observationCount: data.packet.observationCount,
scope: data.packet.scope,
+ summary: data.packet.summary,
latestObserver: {
id: data.observation.observerId,
displayName: data.observation.observerName,
diff --git a/src/types/api.ts b/src/types/api.ts
index f50ac80..e8fbf13 100644
--- a/src/types/api.ts
+++ b/src/types/api.ts
@@ -35,6 +35,7 @@ export interface PacketSummary {
observationCount: number;
latestObserver?: LatestObserver;
scope?: string; // matched transport scope name, e.g. "#bc"
+ summary?: string; // packet-derived display text, currently the advertised name
}
export interface ResolvedNode {
diff --git a/src/types/ws.ts b/src/types/ws.ts
index 7701ccd..3b72345 100644
--- a/src/types/ws.ts
+++ b/src/types/ws.ts
@@ -51,6 +51,7 @@ export interface WsPacketObservation {
isFirstObservation: boolean;
observationCount: number;
scope?: string; // matched transport scope name; omitted when none matched
+ summary?: string;
};
observation: {
observerId: string;
diff --git a/tests/features/packets/PacketTableHeader.test.tsx b/tests/features/packets/PacketTableHeader.test.tsx
index de254c0..4eaa633 100644
--- a/tests/features/packets/PacketTableHeader.test.tsx
+++ b/tests/features/packets/PacketTableHeader.test.tsx
@@ -6,7 +6,7 @@ import { GRID_TEMPLATE } from "../../../src/features/packets/packet-grid";
describe("PacketTableHeader", () => {
it("declares every column heading", () => {
render();
- for (const h of ["Hash", "Type", "Route", "Obs", "Hops", "Hash Size", "Src → Dst", "IATA", "Age"]) {
+ for (const h of ["Hash", "Type", "Route", "Obs", "Hops", "Hash Size", "Summary / Src → Dst", "IATA", "Age"]) {
expect(screen.getByText(h)).toBeInTheDocument();
}
});
diff --git a/tests/features/packets/PacketTableRow.test.tsx b/tests/features/packets/PacketTableRow.test.tsx
index 27435ae..2e11485 100644
--- a/tests/features/packets/PacketTableRow.test.tsx
+++ b/tests/features/packets/PacketTableRow.test.tsx
@@ -23,6 +23,13 @@ const observer = (
};
describe("PacketTableRow", () => {
+ it("keeps endpoints visible alongside a payload summary", () => {
+ render( {}} />);
+ expect(screen.getByText("Packet summary")).toBeInTheDocument();
+ expect(screen.getByText("Source node")).toBeInTheDocument();
+ expect(screen.getByText("Destination node")).toBeInTheDocument();
+ });
+
it("exposes one button carrying the expansion state", () => {
render( {}} />);
const btn = screen.getByRole("button");
diff --git a/tests/features/packets/packet-summary.test.tsx b/tests/features/packets/packet-summary.test.tsx
new file mode 100644
index 0000000..ca7129f
--- /dev/null
+++ b/tests/features/packets/packet-summary.test.tsx
@@ -0,0 +1,26 @@
+import { describe, it, expect, vi } from "vitest";
+import { render, screen, fireEvent } from "@testing-library/react";
+import { PacketRow } from "../../../src/features/packets/PacketRow";
+import { PacketTableRow } from "../../../src/features/packets/PacketTableRow";
+
+const packet = {
+ packetHash: "AA11BB22", payloadType: 4, payloadTypeName: "ADVERT",
+ routeType: 1, routeTypeName: "FLOOD", firstHeardAt: 1, lastHeardAt: 2, observationCount: 1,
+};
+
+describe.each([["mobile", PacketRow], ["desktop", PacketTableRow]] as const)("%s packet summary", (_name, Row) => {
+ it("shows the packet's advertised name and keeps row selection working", () => {
+ const toggle = vi.fn();
+ render(
);
+ const summary = screen.getByText("MD00-Repeater 📡");
+ expect(summary).toHaveAttribute("title", "MD00-Repeater 📡");
+ fireEvent.click(summary);
+ expect(toggle).toHaveBeenCalledTimes(1);
+ });
+
+ it("supports rows from an older server without summaries", () => {
+ render( {}} />);
+ expect(screen.getByText(/AA11/)).toBeInTheDocument();
+ expect(screen.queryByText("MD00-Repeater 📡")).not.toBeInTheDocument();
+ });
+});
diff --git a/tests/features/packets/usePackets.test.tsx b/tests/features/packets/usePackets.test.tsx
index ba98a8f..b0198d1 100644
--- a/tests/features/packets/usePackets.test.tsx
+++ b/tests/features/packets/usePackets.test.tsx
@@ -360,6 +360,20 @@ describe("usePackets live heard window", () => {
const flushRaf = () => rafCallbacks.splice(0).forEach((cb) => cb(0));
+ it("keeps summaries from history and passes summaries through live observations", async () => {
+ getPackets.mockResolvedValue({ items: [{ ...packet("history"), summary: "Historical advert" }], nextCursor: null });
+ const { result } = renderHook(() => usePackets(), { wrapper });
+ await waitFor(() => expect(result.current.allPackets.find((p) => p.packetHash === "history")?.summary).toBe("Historical advert"));
+ const event = observation("live");
+ event.packet.summary = "Live advert 📡";
+ act(() => {
+ result.current.handlePacketObservation(event);
+ flushRaf();
+ });
+ expect(result.current.allPackets.find((p) => p.packetHash === "live")?.summary).toBe("Live advert 📡");
+ expect(result.current.allPackets.find((p) => p.packetHash === "history")?.summary).toBe("Historical advert");
+ });
+
// Each WS message carries only its own heardAt, so a second observation of the same packet used to
// collapse the window to a single instant — the expanded row then read "spread 0.000s".
it("widens the heard window across observations instead of collapsing it", async () => {