From d01fc57989dc26ad2adb2a43b706e16e2fe2a018 Mon Sep 17 00:00:00 2001 From: CarterPerez-dev Date: Sun, 3 May 2026 09:12:36 -0400 Subject: [PATCH] fix(monitor/frontend): DShield panel matches actual snapshot.scan_firehose shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The panel crashed at runtime with "(ds.topports ?? []).slice is not a function" because Plan 5 assumed shapes that didn't match what the Phase 2 DShield collector actually emits. Reality: topports: Record // dict keyed "0".."N", not Array port field: targetport // not "port" topips[].source // the IP — not "ip" topips[].reports // the count — not "records" topips // has no country/CC field dailysummary[].date // ISO date string, ascending order toArray() helper accepts both Record and T[] defensively (so either shape works if the backend changes its mind). Both lists sort by rank before slicing. The CC column is dropped (data doesn't carry it); its slot becomes the per-source target count which the data does carry. Daily summary picks the latest entry by date instead of index 0 (the order is ascending, so [0] would be the oldest day). Bigger discovery: most snapshot keys (cve_new / kev_added / ransomware_victim / coinbase_price / etc.) are LATEST-event payloads, not the recent-list arrays Plan 5 anticipated. Tasks 15-21 will need per-topic Zustand stores that accumulate over time, populated by Task 24 from snapshot bootstrap + WS messages. Will discuss with Carter before committing to that architecture. --- .../src/pages/panels/DShieldPanel.tsx | 71 ++++++++++++++----- 1 file changed, 52 insertions(+), 19 deletions(-) diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/DShieldPanel.tsx b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/DShieldPanel.tsx index 708c5b44..30964cec 100644 --- a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/DShieldPanel.tsx +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/DShieldPanel.tsx @@ -11,24 +11,29 @@ const THOUSAND = 1_000 const MILLION = 1_000_000 interface DShieldPort { - port: number + rank: number + targetport: number records: number + targets: number + sources: number } interface DShieldSource { - ip: string - country?: string - records: number + rank: number + source: string + reports: number + targets: number } interface DShieldDailySummary { + date: string records: number sources: number targets: number } interface DShieldData { - topports?: DShieldPort[] + topports?: Record | DShieldPort[] topips?: DShieldSource[] dailysummary?: DShieldDailySummary[] } @@ -36,7 +41,18 @@ interface DShieldData { export function DShieldPanel(): React.ReactElement { const { data } = useSnapshot() const ds = (data?.scan_firehose as DShieldData | undefined) ?? {} - const summary = ds.dailysummary?.[0] + + const ports = toArray(ds.topports) + .slice() + .sort((a, b) => a.rank - b.rank) + .slice(0, PORT_ROW_LIMIT) + + const sources = (ds.topips ?? []) + .slice() + .sort((a, b) => a.rank - b.rank) + .slice(0, SOURCE_ROW_LIMIT) + + const summary = pickLatestSummary(ds.dailysummary) return ( Port - Hits / 24h + Hits + Src - {(ds.topports ?? []).slice(0, PORT_ROW_LIMIT).map((p) => ( - - {p.port} - {p.records.toLocaleString()} + {ports.map((p) => ( + + {p.targetport} + {fmtN(p.records)} + {fmtN(p.sources)} ))} @@ -67,16 +85,16 @@ export function DShieldPanel(): React.ReactElement { Source IP - CC - Hits + Reports + Tgt - {(ds.topips ?? []).slice(0, SOURCE_ROW_LIMIT).map((s) => ( - - {s.ip} - {s.country ?? '—'} - {s.records.toLocaleString()} + {sources.map((s) => ( + + {s.source} + {fmtN(s.reports)} + {fmtN(s.targets)} ))} @@ -86,7 +104,7 @@ export function DShieldPanel(): React.ReactElement { {summary && (

{fmtN(summary.records)} records · {fmtN(summary.sources)} sources ·{' '} - {fmtN(summary.targets)} targets — last 24h + {fmtN(summary.targets)} targets — {summary.date}

)}
@@ -95,6 +113,21 @@ export function DShieldPanel(): React.ReactElement { DShieldPanel.displayName = 'DShieldPanel' +function toArray(v: Record | T[] | undefined): T[] { + if (!v) return [] + if (Array.isArray(v)) return v + return Object.values(v) +} + +function pickLatestSummary( + list: DShieldDailySummary[] | undefined +): DShieldDailySummary | undefined { + if (!list || list.length === 0) return undefined + return list.reduce((latest, entry) => + entry.date > latest.date ? entry : latest + ) +} + function fmtN(n: number): string { if (n >= MILLION) return `${(n / MILLION).toFixed(1)}M` if (n >= THOUSAND) return `${(n / THOUSAND).toFixed(1)}k`