import React, { useEffect, useRef, useState } from 'react'; import { useSearchParams, useNavigate } from 'react-router-dom'; import { ChevronLeft, ChevronRight, ChevronRight as ChevR, Filter, Crosshair, Search, } from '../icons'; import api from '../utils/api'; import EmptyState from './EmptyState/EmptyState'; import { useFocusSearch } from '../hooks/useFocusSearch'; import { useCampaignStream } from './useCampaignStream'; import './Dashboard.css'; interface CampaignEntry { uuid: string; schema_version: number; first_seen_at: string | null; last_seen_at: string | null; updated_at: string; confidence: number | null; identity_count: number; ja3_hashes: string | null; hassh_hashes: string | null; payload_simhashes: string | null; c2_endpoints: string | null; merged_into_uuid: string | null; } const safeListLen = (raw: string | null): number => { if (!raw) return 0; try { const parsed = JSON.parse(raw); return Array.isArray(parsed) ? parsed.length : 0; } catch { return 0; } }; const timeAgo = (dateStr: string | null): string => { if (!dateStr) return '—'; const diff = Date.now() - new Date(dateStr).getTime(); const mins = Math.floor(diff / 60000); if (mins < 1) return 'just now'; if (mins < 60) return `${mins}m ago`; const hrs = Math.floor(mins / 60); if (hrs < 24) return `${hrs}h ago`; return `${Math.floor(hrs / 24)}d ago`; }; const Campaigns: React.FC = () => { const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const query = (searchParams.get('q') || '').toLowerCase(); const page = parseInt(searchParams.get('page') || '1'); const [campaigns, setCampaigns] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(true); const [searchInput, setSearchInput] = useState(searchParams.get('q') || ''); const searchRef = useRef(null); useFocusSearch(searchRef); const limit = 50; const fetchCampaigns = async () => { setLoading(true); try { const offset = (page - 1) * limit; const res = await api.get(`/campaigns?limit=${limit}&offset=${offset}`); setCampaigns(res.data.data ?? []); setTotal(res.data.total ?? 0); } catch (err) { console.error('Failed to fetch campaigns', err); } finally { setLoading(false); } }; useEffect(() => { fetchCampaigns(); }, [page]); useCampaignStream({ enabled: true, onEvent: () => { fetchCampaigns(); }, }); const handleSearch = (e: React.FormEvent) => { e.preventDefault(); setSearchParams({ q: searchInput, page: '1' }); }; const setPage = (p: number) => setSearchParams({ q: searchParams.get('q') || '', page: p.toString() }); const totalPages = Math.max(1, Math.ceil(total / limit)); const visible = query ? campaigns.filter((c) => c.uuid.toLowerCase().includes(query)) : campaigns; const totalIdentities = campaigns.reduce((sum, c) => sum + c.identity_count, 0); return (

CAMPAIGN CLUSTERING

{total.toLocaleString()} CAMPAIGNS · {totalIdentities} IDENTITIES GROUPED
setSearchInput(e.target.value)} />
{visible.length.toLocaleString()} CAMPAIGNS SHOWN
Page {page} of {totalPages}
{visible.length > 0 ? visible.map((c) => ( navigate(`/campaigns/${c.uuid}`)} > )) : ( )}
UUID FIRST SEEN LAST SEEN FINGERPRINTS INFRA IDENTITIES CONFIDENCE
{c.uuid.slice(0, 12)}… {timeAgo(c.first_seen_at)} {timeAgo(c.last_seen_at)} {safeListLen(c.ja3_hashes)} JA3{' '} {safeListLen(c.hassh_hashes)} HASSH {safeListLen(c.payload_simhashes)} PAYLOAD{' '} {safeListLen(c.c2_endpoints)} C2 {c.identity_count} {c.confidence !== null ? c.confidence.toFixed(2) : '—'}
); }; export default Campaigns;