From 161f481576ccaa23ed2cd9024bd70a5d93e22d0a Mon Sep 17 00:00:00 2001 From: MrAlders0n Date: Mon, 27 Jul 2026 18:01:32 -0400 Subject: [PATCH] Carry path and endpoint fields from WS observations into packet summaries --- src/features/packets/usePackets.ts | 5 ++ src/types/api.ts | 8 +++ src/types/ws.ts | 4 +- tests/features/packets/usePackets.test.tsx | 64 ++++++++++++++++++++++ 4 files changed, 80 insertions(+), 1 deletion(-) diff --git a/src/features/packets/usePackets.ts b/src/features/packets/usePackets.ts index 35e704e..5eb8dfd 100644 --- a/src/features/packets/usePackets.ts +++ b/src/features/packets/usePackets.ts @@ -159,6 +159,11 @@ export function usePackets(frozen: boolean = false, serverFilter: PacketServerFi id: data.observation.observerId, displayName: data.observation.observerName, iata: data.observation.iata, + pathLength: data.observation.pathLength, + pathBytes: data.observation.pathBytes, + // WS nulls these when the payload type carries no endpoint; the REST shape uses undefined + resolvedSource: data.observation.resolvedSource ?? undefined, + resolvedDestination: data.observation.resolvedDestination ?? undefined, }, }; diff --git a/src/types/api.ts b/src/types/api.ts index 86dfa73..28d0913 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -12,6 +12,14 @@ export interface LatestObserver { id: string; displayName?: string; iata: string; + // path fields land on the REST list from beacon-server ae0669c, and on live rows via the WS feed; + // resolvedSource/Destination are WS-only for now — the list endpoints leave them nil on purpose. + pathLength?: PathLength; + pathBytes?: string; + resolvedSource?: ResolvedHop; + resolvedDestination?: ResolvedHop; + // per-hop resolved path; WS-only, and only when the connection opts into configure{resolvePath}. + resolvedPath?: ResolvedHop[]; } // packet list and detail shapes diff --git a/src/types/ws.ts b/src/types/ws.ts index ea65403..7701ccd 100644 --- a/src/types/ws.ts +++ b/src/types/ws.ts @@ -1,6 +1,6 @@ import type { ChannelMessage } from "../features/channels/types"; import type { NodeIATA } from "../features/nodes/types"; -import type { ResolvedHop } from "./api"; +import type { PathLength, ResolvedHop } from "./api"; // individual server-sent message shapes @@ -60,6 +60,8 @@ export interface WsPacketObservation { rssi: number; snr: number; sourceBroker: string; + pathLength?: PathLength; + pathBytes?: string; // per-hop resolved path; populated only when the connection opts in via configure{resolvePath}, // null otherwise. Same shape as the REST Observation.resolvedPath. resolvedPath?: ResolvedHop[] | null; diff --git a/tests/features/packets/usePackets.test.tsx b/tests/features/packets/usePackets.test.tsx index ea5ea7f..b71f6cd 100644 --- a/tests/features/packets/usePackets.test.tsx +++ b/tests/features/packets/usePackets.test.tsx @@ -215,6 +215,70 @@ describe("usePackets server filter", () => { }); }); +describe("usePackets path and endpoint fields", () => { + let qc: QueryClient; + let rafCallbacks: FrameRequestCallback[]; + + beforeEach(() => { + getPackets.mockReset(); + getPackets.mockResolvedValue({ items: [], nextCursor: null }); + qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + + rafCallbacks = []; + vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) => { + rafCallbacks.push(cb); + return rafCallbacks.length; + }); + vi.stubGlobal("cancelAnimationFrame", () => {}); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + const wrapper = ({ children }: { children: ReactNode }) => ( + {children} + ); + + it("carries path and endpoint fields from the WS observation into latestObserver", () => { + const { result } = renderHook(() => usePackets(false, undefined), { wrapper }); + + act(() => { + result.current.handlePacketObservation({ + packetHash: "AA11", + packet: { + payloadType: 1, + payloadTypeName: "ADVERT", + routeType: 1, + routeTypeName: "FLOOD", + isFirstObservation: true, + observationCount: 1, + }, + observation: { + observerId: "obs-1", + observerName: "Raven", + iata: "YVR", + heardAt: 1700000000, + rssi: -94, + snr: -7.5, + sourceBroker: "b1", + pathLength: { raw: "42", hashSize: 1, hopCount: 2 }, + pathBytes: "7fa4", + resolvedSource: { confidence: "high", nodes: [{ id: "n1", publicKey: "ab", name: "Salish" }] }, + resolvedDestination: null, + }, + }); + rafCallbacks.splice(0).forEach((cb) => cb(0)); + }); + + const obs = result.current.allPackets[0]!.latestObserver; + expect(obs?.pathLength).toEqual({ raw: "42", hashSize: 1, hopCount: 2 }); + expect(obs?.pathBytes).toBe("7fa4"); + expect(obs?.resolvedSource?.nodes[0]!.name).toBe("Salish"); + expect(obs?.resolvedDestination).toBeUndefined(); + }); +}); + function observation(hash: string): WsPacketObservation["data"] { return { packetHash: hash,