diff --git a/decnet_web/src/components/AttackerDetail.tsx b/decnet_web/src/components/AttackerDetail.tsx
index 0007fdd..e50dc51 100644
--- a/decnet_web/src/components/AttackerDetail.tsx
+++ b/decnet_web/src/components/AttackerDetail.tsx
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
-import { ArrowLeft, ChevronLeft, ChevronRight, Crosshair, Fingerprint, Shield, Clock, Wifi, Lock, FileKey } from 'lucide-react';
+import { ArrowLeft, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Crosshair, Fingerprint, Shield, Clock, Wifi, Lock, FileKey } from 'lucide-react';
import api from '../utils/api';
import './Dashboard.css';
@@ -312,6 +312,31 @@ const FingerprintGroup: React.FC<{ fpType: string; items: any[] }> = ({ fpType,
);
};
+// ─── Collapsible section ────────────────────────────────────────────────────
+
+const Section: React.FC<{
+ title: React.ReactNode;
+ right?: React.ReactNode;
+ open: boolean;
+ onToggle: () => void;
+ children: React.ReactNode;
+}> = ({ title, right, open, onToggle, children }) => (
+
+
+
+ {open ? : }
+
{title}
+
+ {right &&
e.stopPropagation()}>{right}
}
+
+ {open && children}
+
+);
+
// ─── Main component ─────────────────────────────────────────────────────────
const AttackerDetail: React.FC = () => {
@@ -322,6 +347,16 @@ const AttackerDetail: React.FC = () => {
const [error, setError] = useState(null);
const [serviceFilter, setServiceFilter] = useState(null);
+ // Section collapse state
+ const [openSections, setOpenSections] = useState>({
+ timeline: true,
+ services: true,
+ deckies: true,
+ commands: true,
+ fingerprints: true,
+ });
+ const toggle = (key: string) => setOpenSections((prev) => ({ ...prev, [key]: !prev[key] }));
+
// Commands pagination state
const [commands, setCommands] = useState([]);
const [cmdTotal, setCmdTotal] = useState(0);
@@ -441,10 +476,7 @@ const AttackerDetail: React.FC = () => {
{/* Timestamps */}
-
-
-
TIMELINE
-
+
toggle('timeline')}>
FIRST SEEN:
@@ -459,13 +491,10 @@ const AttackerDetail: React.FC = () => {
{new Date(attacker.updated_at).toLocaleString()}
-
+
{/* Services */}
-
-
-
SERVICES TARGETED
-
+
toggle('services')}>
{attacker.services.length > 0 ? attacker.services.map((svc) => {
const isActive = serviceFilter === svc;
@@ -491,13 +520,10 @@ const AttackerDetail: React.FC = () => {
No services recorded
)}
-
+
{/* Deckies & Traversal */}
-
-
-
DECKY INTERACTIONS
-
+
toggle('deckies')}>
{attacker.traversal_path ? (
@@ -515,39 +541,40 @@ const AttackerDetail: React.FC = () => {
)}
-
+
{/* Commands */}
{(() => {
const cmdTotalPages = Math.ceil(cmdTotal / cmdLimit);
return (
-
-
-
COMMANDS ({cmdTotal}{serviceFilter ? ` ${serviceFilter.toUpperCase()}` : ''})
- {cmdTotalPages > 1 && (
-
-
- Page {cmdPage} of {cmdTotalPages}
-
-
-
-
-
+
COMMANDS ({cmdTotal}{serviceFilter ? ` ${serviceFilter.toUpperCase()}` : ''})>}
+ open={openSections.commands}
+ onToggle={() => toggle('commands')}
+ right={openSections.commands && cmdTotalPages > 1 ? (
+
+
+ Page {cmdPage} of {cmdTotalPages}
+
+
+
+
- )}
-
+
+ ) : undefined}
+ >
{commands.length > 0 ? (
@@ -578,7 +605,7 @@ const AttackerDetail: React.FC = () => {
{serviceFilter ? `NO ${serviceFilter.toUpperCase()} COMMANDS CAPTURED` : 'NO COMMANDS CAPTURED'}
)}
-
+
);
})()}
@@ -605,16 +632,16 @@ const AttackerDetail: React.FC = () => {
const passiveTypes = ['ja3', 'ja4l', 'tls_resumption', 'tls_certificate', 'http_useragent', 'vnc_client_version'];
const knownTypes = [...activeTypes, ...passiveTypes];
const unknownTypes = Object.keys(groups).filter((t) => !knownTypes.includes(t));
- const orderedTypes = [...activeTypes, ...passiveTypes, ...unknownTypes].filter((t) => groups[t]);
const hasActive = activeTypes.some((t) => groups[t]);
const hasPassive = [...passiveTypes, ...unknownTypes].some((t) => groups[t]);
return (
-
-
-
FINGERPRINTS ({filteredFps.length}{serviceFilter ? ` / ${attacker.fingerprints.length}` : ''})
-
+
FINGERPRINTS ({filteredFps.length}{serviceFilter ? ` / ${attacker.fingerprints.length}` : ''})>}
+ open={openSections.fingerprints}
+ onToggle={() => toggle('fingerprints')}
+ >
{filteredFps.length > 0 ? (
{/* Active probes section */}
@@ -652,7 +679,7 @@ const AttackerDetail: React.FC = () => {
{serviceFilter ? `NO ${serviceFilter.toUpperCase()} FINGERPRINTS CAPTURED` : 'NO FINGERPRINTS CAPTURED'}
)}
-
+
);
})()}