import React, { useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import './Dashboard.css'; import { Shield, Users, Activity, Clock, Paperclip, Crosshair, Flame, Archive, ShieldOff, Server } from 'lucide-react'; import { parseEventBody } from '../utils/parseEventBody'; import ArtifactDrawer from './ArtifactDrawer'; import EmptyState from './EmptyState/EmptyState'; interface Stats { total_logs: number; unique_attackers: number; active_deckies: number; deployed_deckies: number; } interface LogEntry { id: number; timestamp: string; decky: string; service: string; event_type: string | null; attacker_ip: string; raw_line: string; fields: string | null; msg: string | null; severity?: string; is_bounty?: boolean; } interface DashboardProps { searchQuery: string; } type ThreatLevel = 'nominal' | 'elevated' | 'critical'; const SPARK_LEN = 12; function Sparkline({ data, alert }: { data: number[]; alert?: boolean }) { const max = Math.max(...data, 1); return (
{data.map((v, i) => ( ))}
); } function rollWindow(prev: number[], next: number): number[] { const out = prev.slice(-SPARK_LEN + 1); out.push(next); while (out.length < SPARK_LEN) out.unshift(0); return out; } function computeThreat(hits5m: number): ThreatLevel { if (hits5m > 100) return 'critical'; if (hits5m > 50) return 'elevated'; return 'nominal'; } function getSector(): string { try { const raw = localStorage.getItem('decnet_tweaks'); if (!raw) return 'PRODUCTION'; const t = JSON.parse(raw); return (t?.sector || 'PRODUCTION').toString().toUpperCase(); } catch { return 'PRODUCTION'; } } const Dashboard: React.FC = ({ searchQuery }) => { const navigate = useNavigate(); const [stats, setStats] = useState(null); const [logs, setLogs] = useState([]); const [loading, setLoading] = useState(true); const [artifact, setArtifact] = useState<{ decky: string; storedAs: string; fields: Record } | null>(null); const [newestLogId, setNewestLogId] = useState(null); const [sparkTotal, setSparkTotal] = useState(() => Array(SPARK_LEN).fill(0)); const [sparkAttackers, setSparkAttackers] = useState(() => Array(SPARK_LEN).fill(0)); const [sparkBounties, setSparkBounties] = useState(() => Array(SPARK_LEN).fill(0)); const lastStatsRef = useRef<{ total: number; uniq: number; bounties: number } | null>(null); const eventSourceRef = useRef(null); const reconnectTimerRef = useRef | null>(null); useEffect(() => { const connect = () => { if (eventSourceRef.current) eventSourceRef.current.close(); const token = localStorage.getItem('token'); const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000/api/v1'; let url = `${baseUrl}/stream?token=${token}`; if (searchQuery) url += `&search=${encodeURIComponent(searchQuery)}`; const es = new EventSource(url); eventSourceRef.current = es; es.onmessage = (event) => { try { const payload = JSON.parse(event.data); if (payload.type === 'logs') { const incoming: LogEntry[] = payload.data; if (incoming.length > 0) { setNewestLogId(incoming[0].id); } setLogs(prev => [...incoming, ...prev].slice(0, 100)); } else if (payload.type === 'stats') { setStats(payload.data); setLoading(false); } } catch (err) { console.error('Failed to parse SSE payload', err); } }; es.onerror = () => { es.close(); eventSourceRef.current = null; reconnectTimerRef.current = setTimeout(connect, 3000); }; }; connect(); return () => { if (reconnectTimerRef.current) clearTimeout(reconnectTimerRef.current); if (eventSourceRef.current) eventSourceRef.current.close(); }; }, [searchQuery]); // Tick once a second so the 5-min rolling window stays accurate even // when logs haven't arrived. const [nowTick, setNowTick] = useState(() => Date.now()); useEffect(() => { const iv = setInterval(() => setNowTick(Date.now()), 1000); return () => clearInterval(iv); }, []); // Derived metrics from live log buffer const { hits5m, alertCount, uniqueAttackers5m, bountiesCount, deckiesUnderSiege, topAttackers } = useMemo(() => { const cutoff = nowTick - 5 * 60_000; const recent = logs.filter(l => { const t = Date.parse(l.timestamp); return !isNaN(t) && t >= cutoff; }); const alertN = recent.filter(l => l.severity === 'warn' || l.is_bounty).length; const uniq = new Set(recent.map(l => l.attacker_ip)).size; const bounties = logs.filter(l => l.is_bounty).length; const deckyHits = new Map(); for (const l of recent) deckyHits.set(l.decky, (deckyHits.get(l.decky) || 0) + 1); const siege = Array.from(deckyHits.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 5) .map(([name, hits]) => ({ name, hits, status: hits > 30 ? 'hot' : hits > 10 ? 'warn' : 'active', })); const attackerHits = new Map(); for (const l of logs) attackerHits.set(l.attacker_ip, (attackerHits.get(l.attacker_ip) || 0) + 1); const maxAttackerHits = Math.max(1, ...attackerHits.values()); const top = Array.from(attackerHits.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 4) .map(([ip, hits]) => ({ ip, hits, pct: Math.min(100, (hits / maxAttackerHits) * 100), hot: hits > maxAttackerHits * 0.6, })); return { hits5m: recent.length, alertCount: alertN, uniqueAttackers5m: uniq, bountiesCount: bounties, deckiesUnderSiege: siege, topAttackers: top, }; }, [logs, nowTick]); const threat = computeThreat(hits5m); // Broadcast stats + threat for Layout's listener useEffect(() => { if (!stats) return; window.dispatchEvent(new CustomEvent('decnet:stats', { detail: { ...stats, threat, hits_5m: hits5m, alert_count: alertCount }, })); }, [stats, threat, hits5m, alertCount]); // Roll sparklines on each stats frame useEffect(() => { if (!stats) return; const total = stats.total_logs; const uniq = stats.unique_attackers; const last = lastStatsRef.current; if (last) { const dTotal = Math.max(0, total - last.total); const dUniq = Math.max(0, uniq - last.uniq); const dBounties = Math.max(0, bountiesCount - last.bounties); setSparkTotal(prev => rollWindow(prev, dTotal)); setSparkAttackers(prev => rollWindow(prev, dUniq)); setSparkBounties(prev => rollWindow(prev, dBounties)); } lastStatsRef.current = { total, uniq, bounties: bountiesCount }; }, [stats, bountiesCount]); if (loading && !stats) return
INITIALIZING SENSORS...
; const sector = getSector(); return (

DASHBOARD

SECTOR · {sector} · LIVE
LIVE
{threat === 'critical' && (
ACTIVE BREACH — {hits5m} hits in last 5 min · {uniqueAttackers5m} attackers
)}
TOTAL INTERACTIONS
{(stats?.total_logs ?? 0).toLocaleString()}
+{hits5m} in last 5m
UNIQUE ATTACKERS
{(stats?.unique_attackers ?? 0).toLocaleString()}
{uniqueAttackers5m} active in 5m
ACTIVE DECKIES
{stats?.active_deckies ?? 0} / {stats?.deployed_deckies ?? 0}
OF TOTAL FLEET
BOUNTIES CAPTURED
{bountiesCount.toLocaleString()}
THIS SESSION
LIVE INTERACTION FEED LIVE
{logs.length} RECENT
{logs.length > 0 ? logs.slice(0, 14).map(log => { let parsedFields: Record = {}; if (log.fields) { try { parsedFields = JSON.parse(log.fields); } catch { // ignore } } let msgHead: string | null = null; let msgTail: string | null = null; if (Object.keys(parsedFields).length === 0) { const parsed = parseEventBody(log.msg); parsedFields = parsed.fields; msgHead = parsed.head; msgTail = parsed.tail; } else if (log.msg && log.msg !== '-') { msgTail = log.msg; } const isAlert = log.severity === 'warn' || log.is_bounty; const isNew = log.id === newestLogId; return ( ); }) : ( )}
TIME DECKY SVC ATTACKER EVENT
{new Date(log.timestamp).toLocaleTimeString()} {log.is_bounty ? BOUNTY : } {log.decky} {log.service} {log.attacker_ip}
{(() => { const et = log.event_type && log.event_type !== '-' ? log.event_type : null; const parts = [et, msgHead].filter(Boolean) as string[]; return ( <> {parts.join(' · ')} {msgTail && ( {parts.length ? ' — ' : ''}{msgTail} )} ); })()}
{Object.keys(parsedFields).length > 0 && (
{parsedFields.stored_as != null && ( )} {Object.entries(parsedFields) .filter(([k]) => k !== 'meta_json_b64' && k !== 'stored_as') .map(([k, v]) => { const rendered = typeof v === 'object' ? JSON.stringify(v) : String(v); return ( {k}: {rendered} ); })}
)}
DECKIES UNDER SIEGE
{deckiesUnderSiege.length > 0 ? (
{deckiesUnderSiege.map(d => (
window.dispatchEvent(new CustomEvent('decnet:cmd', { detail: { id: 'filter-decky', payload: d.name } }))} > {d.name}
{d.hits}
))}
) : ( )}
TOP ATTACKERS
{topAttackers.length > 0 ? (
{topAttackers.map(a => (
window.dispatchEvent(new CustomEvent('decnet:cmd', { detail: { id: 'filter-attacker', payload: a.ip } }))} > ?? {a.ip}
{a.hits}
))}
) : ( )}
{artifact && ( } onClose={() => setArtifact(null)} /> )}
); }; export default Dashboard;