feat(web-ui): IdentityDetail page + conditional Identity badge on AttackerDetail

Third of the five-step identity-resolution substrate. Frontend hooks
into the empty /api/v1/identities/* surface from commit 2; renders
nothing visible when identity_id is null (which is the universal state
until the clusterer ships).

* decnet_web/src/components/IdentityDetail.tsx — new page. Header with
  uuid + optional CAMPAIGN / MERGED-INTO badges, stats row
  (observations / JA3 / HASSH / payloads / C2), fingerprint tag lists
  parsed from the JSON-in-TEXT columns, observations table that links
  back to AttackerDetail, conditional analyst-notes panel.
* decnet_web/src/components/AttackerDetail.tsx — IDENTITY badge
  inserted in the header row alongside TRAVERSAL. Clicking navigates
  to /identities/<uuid>. AttackerData interface gains the optional
  identity_id field.
* decnet_web/src/App.tsx — /identities/:id route + lazy-loaded chunk.

Verified by `tsc --noEmit` (clean) and `vite build` (clean — produces
IdentityDetail-*.js as its own lazy chunk). The repo has no JS test
harness; build + type-check are the gate.
This commit is contained in:
2026-04-26 07:12:37 -04:00
parent dc3d08dd41
commit 448212ebcd
3 changed files with 332 additions and 0 deletions

View File

@@ -19,6 +19,7 @@ const LiveLogs = lazy(() => import('./components/LiveLogs'));
const Webhooks = lazy(() => import('./components/Webhooks'));
const Attackers = lazy(() => import('./components/Attackers'));
const AttackerDetail = lazy(() => import('./components/AttackerDetail'));
const IdentityDetail = lazy(() => import('./components/IdentityDetail'));
const Config = lazy(() => import('./components/Config'));
const Bounty = lazy(() => import('./components/Bounty'));
const Credentials = lazy(() => import('./components/Credentials'));
@@ -113,6 +114,7 @@ const AuthedShell: React.FC<AuthedShellProps> = ({ onLogout, onSearch, searchQue
<Route path="/credentials" element={<Credentials />} />
<Route path="/attackers" element={<Attackers />} />
<Route path="/attackers/:id" element={<AttackerDetail />} />
<Route path="/identities/:id" element={<IdentityDetail />} />
<Route path="/config" element={<Config />} />
<Route path="/swarm-updates" element={<RemoteUpdates />} />
<Route path="/swarm/hosts" element={<SwarmHosts />} />