Carry path and endpoint fields from WS observations into packet summaries

This commit is contained in:
MrAlders0n
2026-07-27 18:01:32 -04:00
parent 464e701f76
commit 161f481576
4 changed files with 80 additions and 1 deletions
+5
View File
@@ -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,
},
};
+8
View File
@@ -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
+3 -1
View File
@@ -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;
@@ -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 }) => (
<QueryClientProvider client={qc}>{children}</QueryClientProvider>
);
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,