import React, { useEffect, useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { ArrowLeft, Crosshair, Filter, Fingerprint, Globe, Radio } from '../icons'; import api from '../utils/api'; import EmptyState from './EmptyState/EmptyState'; import { useCampaignStream } from './useCampaignStream'; import './Dashboard.css'; /* * CampaignDetail — read-only view of a campaign-clustered operation. * * Layer above identity resolution. Member identities link back to * IdentityDetail; same visual vocabulary as the rest of the app * (page-header / sections / chips), no inline-style drift. */ interface CampaignData { uuid: string; schema_version: number; first_seen_at: string | null; last_seen_at: string | null; created_at: string; updated_at: string; confidence: number | null; identity_count: number; identity_count_live: number; ja3_hashes: string | null; hassh_hashes: string | null; payload_simhashes: string | null; c2_endpoints: string | null; merged_into_uuid: string | null; notes: string | null; } interface IdentityRow { uuid: string; first_seen_at: string | null; last_seen_at: string | null; observation_count: number; campaign_id: string | null; merged_into_uuid: string | null; } const safeParseJsonList = (raw: string | null): string[] => { if (!raw) return []; try { const parsed = JSON.parse(raw); return Array.isArray(parsed) ? parsed : []; } catch { return []; } }; const CampaignDetail: React.FC = () => { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const [campaign, setCampaign] = useState(null); const [identities, setIdentities] = useState([]); const [identityTotal, setIdentityTotal] = useState(0); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { if (!id) return; const fetchCampaign = async () => { setLoading(true); try { const res = await api.get(`/campaigns/${id}`); setCampaign(res.data); setError(null); } catch (err: any) { if (err.response?.status === 404) { setError('CAMPAIGN NOT FOUND'); } else { setError('FAILED TO LOAD CAMPAIGN'); } } finally { setLoading(false); } }; fetchCampaign(); }, [id]); useEffect(() => { if (!id) return; const fetchIdentities = async () => { try { const res = await api.get(`/campaigns/${id}/identities?limit=50&offset=0`); setIdentities(res.data.data ?? []); setIdentityTotal(res.data.total ?? 0); } catch { setIdentities([]); setIdentityTotal(0); } }; fetchIdentities(); }, [id]); // Refetch when a campaign event references this uuid. useCampaignStream({ enabled: !!id, onEvent: (ev) => { if (!id) return; const refs = new Set(); const addUuid = (v: unknown) => { if (typeof v === 'string') refs.add(v); }; const payload = ev.payload || {}; addUuid(payload.campaign_uuid); addUuid(payload.winner_uuid); addUuid(payload.loser_uuid); addUuid(payload.resurrected_uuid); addUuid(payload.former_winner_uuid); if (refs.has(id)) { api.get(`/campaigns/${id}`).then((res) => setCampaign(res.data)).catch(() => {}); api.get(`/campaigns/${id}/identities?limit=50&offset=0`) .then((res) => { setIdentities(res.data.data ?? []); setIdentityTotal(res.data.total ?? 0); }) .catch(() => {}); } }, }); if (loading) { return (
); } if (error || !campaign) { return (
); } const ja3List = safeParseJsonList(campaign.ja3_hashes); const hasshList = safeParseJsonList(campaign.hassh_hashes); const payloadList = safeParseJsonList(campaign.payload_simhashes); const c2List = safeParseJsonList(campaign.c2_endpoints); return (

CAMPAIGN · {campaign.uuid.slice(0, 12)}…

{campaign.merged_into_uuid && ( navigate(`/campaigns/${campaign.merged_into_uuid}`)} title="Soft-merged. Click to view canonical winner." > MERGED → {campaign.merged_into_uuid.slice(0, 8)}… )}
{campaign.identity_count_live} IDENTITIES · {' '}{ja3List.length} JA3 · {hasshList.length} HASSH · {' '}{payloadList.length} PAYLOAD · {c2List.length} C2 {campaign.confidence !== null && ( <> · CONFIDENCE {campaign.confidence.toFixed(3)} )} {' '}· SCHEMA v{campaign.schema_version}
{(ja3List.length > 0 || hasshList.length > 0 || c2List.length > 0) && (
AGGREGATED FINGERPRINTS
{ja3List.length > 0 && ( } label="JA3" items={ja3List} /> )} {hasshList.length > 0 && ( } label="HASSH" items={hasshList} /> )} {c2List.length > 0 && ( } label="C2 ENDPOINTS" items={c2List} /> )}
)}
{identityTotal} IDENTITIES IN THIS CAMPAIGN
{identities.length === 0 ? ( ) : ( {identities.map((ident) => ( navigate(`/identities/${ident.uuid}`)} > ))}
IDENTITY FIRST SEEN LAST SEEN OBSERVATIONS
{ident.uuid.slice(0, 12)}… {ident.first_seen_at ?? '—'} {ident.last_seen_at ?? '—'} {ident.observation_count}
)}
{campaign.notes && (
ANALYST NOTES
{campaign.notes}
)}
); }; const FingerprintGroup: React.FC<{ icon: React.ReactNode; label: string; items: string[]; }> = ({ icon, label, items }) => (
{icon} {label}
{items.map((v) => ( {v} ))}
); export default CampaignDetail;