From 01c86a486e33c630d329a5831a9d8b8ff81db3b1 Mon Sep 17 00:00:00 2001 From: Jack Kingsman Date: Wed, 8 Apr 2026 14:44:41 -0700 Subject: [PATCH] Add packet feed filters; closes #169. --- frontend/src/components/RawPacketFeedView.tsx | 205 +++++++++++++++--- frontend/src/utils/rawPacketStats.ts | 2 +- 2 files changed, 180 insertions(+), 27 deletions(-) diff --git a/frontend/src/components/RawPacketFeedView.tsx b/frontend/src/components/RawPacketFeedView.tsx index 3015c0f..f51be68 100644 --- a/frontend/src/components/RawPacketFeedView.tsx +++ b/frontend/src/components/RawPacketFeedView.tsx @@ -11,11 +11,14 @@ import { Cell, } from 'recharts'; +import { MeshCoreDecoder, Utils } from '@michaelhart/meshcore-decoder'; + import { RawPacketList } from './RawPacketList'; import { RawPacketInspectorDialog } from './RawPacketDetailModal'; import { Button } from './ui/button'; import type { Channel, Contact, RawPacket } from '../types'; import { + KNOWN_PAYLOAD_TYPES, RAW_PACKET_STATS_WINDOWS, buildRawPacketStatsSnapshot, type NeighborStat, @@ -24,9 +27,26 @@ import { type RawPacketStatsSessionState, type RawPacketStatsWindow, } from '../utils/rawPacketStats'; +import { createDecoderOptions } from '../utils/rawPacketInspector'; import { getContactDisplayName } from '../utils/pubkey'; import { cn } from '@/lib/utils'; +const KNOWN_PAYLOAD_TYPE_SET = new Set(KNOWN_PAYLOAD_TYPES); + +function getPacketTypeName( + packet: RawPacket, + decoderOptions?: ReturnType +): string { + try { + const decoded = MeshCoreDecoder.decode(packet.data, decoderOptions); + if (!decoded.isValid) return 'Unknown'; + const name = Utils.getPayloadTypeName(decoded.payloadType); + return KNOWN_PAYLOAD_TYPE_SET.has(name) ? name : 'Unknown'; + } catch { + return 'Unknown'; + } +} + interface RawPacketFeedViewProps { packets: RawPacket[]; rawPacketStatsSession: RawPacketStatsSessionState; @@ -428,6 +448,48 @@ export function RawPacketFeedView({ const [nowSec, setNowSec] = useState(() => Math.floor(Date.now() / 1000)); const [selectedPacket, setSelectedPacket] = useState(null); const [analyzeModalOpen, setAnalyzeModalOpen] = useState(false); + const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false); + const [enabledTypes, setEnabledTypes] = useState>(() => new Set(KNOWN_PAYLOAD_TYPES)); + + const decoderOptions = useMemo(() => createDecoderOptions(channels), [channels]); + + const packetsWithTypes = useMemo( + () => + packets.map((packet) => ({ + packet, + payloadType: getPacketTypeName(packet, decoderOptions), + })), + [packets, decoderOptions] + ); + + const allTypesEnabled = enabledTypes.size === KNOWN_PAYLOAD_TYPES.length; + + const filteredPackets = useMemo(() => { + if (allTypesEnabled) return packets; + return packetsWithTypes + .filter(({ payloadType }) => enabledTypes.has(payloadType)) + .map(({ packet }) => packet); + }, [packetsWithTypes, enabledTypes, packets, allTypesEnabled]); + + const handleToggleAll = () => { + setEnabledTypes(allTypesEnabled ? new Set() : new Set(KNOWN_PAYLOAD_TYPES)); + }; + + const handleToggleType = (type: string) => { + setEnabledTypes((prev) => { + const next = new Set(prev); + if (next.has(type)) { + next.delete(type); + } else { + next.add(type); + } + return next; + }); + }; + + const handleOnly = (type: string) => { + setEnabledTypes(new Set([type])); + }; useEffect(() => { const interval = window.setInterval(() => { @@ -468,38 +530,129 @@ export function RawPacketFeedView({ ); return ( <> -
-
-

Raw Packet Feed

-

- Collecting stats since {formatTimestamp(rawPacketStatsSession.sessionStartedAt)} -

+
+
+
+

Raw Packet Feed

+

+ Collecting stats since {formatTimestamp(rawPacketStatsSession.sessionStartedAt)} +

+
+
+ + +
-
- - +

+ Collecting stats since {formatTimestamp(rawPacketStatsSession.sessionStartedAt)} + {!mobileFiltersOpen && ( + <> + {' ยท '} + + + )} +

+ + {mobileFiltersOpen && ( +
+ + {KNOWN_PAYLOAD_TYPES.map((type) => ( + + + + + ))} +
+ )} + +
+ + {KNOWN_PAYLOAD_TYPES.map((type) => ( + + + + + ))}
- +