import { useState, useEffect, useRef, useMemo, useCallback } from 'react'; import { useWebSocket } from '../hooks/useWebSocket'; import { formatHashrate } from '../help/fleetFilters'; import './ActivityFeedPage.css'; // ── Event types ─────────────────────────────────────────────────────────── export type ActivityEventKind = | 'connect' | 'disconnect' | 'hashrate' | 'share' | 'alert' | 'command' | 'ai' | 'posture' | 'default'; export interface ActivityEvent { id: string; kind: ActivityEventKind; agentId?: string; agentName?: string; message: string; detail?: string; ts: Date; raw?: unknown; } let _eid = 0; function eid() { return String(++_eid); } // ── Visual config per kind ──────────────────────────────────────────────── const KIND_CONFIG: Record = { connect: { icon: '🟢', label: 'ONLINE', color: '#39ff14' }, disconnect: { icon: '🔴', label: 'OFFLINE', color: '#ff4444' }, hashrate: { icon: '⚡', label: 'HASHRATE', color: '#00e8f5' }, share: { icon: '✅', label: 'SHARE', color: '#b24bf3' }, alert: { icon: '⚠️', label: 'ALERT', color: '#ff6b35' }, command: { icon: '📡', label: 'COMMAND', color: '#ffb020' }, ai: { icon: '🤖', label: 'AI', color: '#ff2da6' }, posture: { icon: '🛡️', label: 'POSTURE', color: '#a8ff78' }, default: { icon: '·', label: 'EVENT', color: '#8899aa' }, }; // ALL_KINDS excludes 'default' — that kind is a fallback sentinel and is never // actually emitted, so it would only create a permanently-zero filter chip. const ALL_KINDS = (Object.keys(KIND_CONFIG) as ActivityEventKind[]).filter( (k) => k !== 'default' ); const MAX_EVENTS = 500; function fmt(d: Date): string { return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); } // ── Event row ───────────────────────────────────────────────────────────── function EventRow({ event }: { event: ActivityEvent }) { const cfg = KIND_CONFIG[event.kind]; return (
{cfg.icon}
{cfg.label} {event.agentName && ( {event.agentName} )} {event.message}
{event.detail && (
{event.detail}
)}
{fmt(event.ts)}
); } // ── Main page ───────────────────────────────────────────────────────────── export default function ActivityFeedPage() { const { isConnected, agents, recentShares, fleetAlerts, commandResults, aiActivity, } = useWebSocket(); const [events, setEvents] = useState([]); const [activeFilters, setActiveFilters] = useState>(new Set(ALL_KINDS)); const [search, setSearch] = useState(''); const [autoScroll, setAutoScroll] = useState(true); const streamRef = useRef(null); const agentMapRef = useRef>(new Map()); // id → name const prevAgentStatus = useRef>({}); // id → status const prevHashrates = useRef>({}); // id → hashrate_15m const prevPosture = useRef>({}); // id → posture_score // Build agent name lookup useEffect(() => { for (const a of agents) agentMapRef.current.set(a.id, a.name); }, [agents]); const push = useCallback((ev: ActivityEvent) => { setEvents((prev) => [ev, ...prev].slice(0, MAX_EVENTS)); }, []); // ── Agent status change events (online / offline) ────────────────────── useEffect(() => { for (const agent of agents) { const prev = prevAgentStatus.current[agent.id]; if (prev === undefined) { // First time we see this agent — synthetic "connect" on page load prevAgentStatus.current[agent.id] = agent.status; if (agent.status === 'online') { push({ id: eid(), kind: 'connect', agentId: agent.id, agentName: agent.name, message: 'came online', detail: `${agent.platform ?? 'unknown'} · ${agent.ip ?? '—'} · ${agent.cpu_cores}c`, ts: new Date(), }); } continue; } if (prev !== agent.status) { prevAgentStatus.current[agent.id] = agent.status; if (agent.status === 'online') { push({ id: eid(), kind: 'connect', agentId: agent.id, agentName: agent.name, message: 'reconnected', detail: `${agent.platform ?? ''} · ${agent.ip ?? '—'}`, ts: new Date(), }); } else { push({ id: eid(), kind: 'disconnect', agentId: agent.id, agentName: agent.name, message: 'went offline', ts: new Date(), }); } } } }, [agents, push]); // ── Hashrate spike events ────────────────────────────────────────────── useEffect(() => { for (const agent of agents) { if (agent.status !== 'online') continue; const prev = prevHashrates.current[agent.id]; const cur = agent.hashrate_15m ?? 0; prevHashrates.current[agent.id] = cur; if (prev === undefined || prev <= 0) continue; const delta = cur - prev; // Only emit if ≥20% change AND at least 100 H/s delta if (Math.abs(delta) >= 100 && Math.abs(delta) / Math.max(prev, 1) >= 0.20) { push({ id: eid(), kind: 'hashrate', agentId: agent.id, agentName: agent.name, message: delta > 0 ? `hashrate up to ${formatHashrate(cur)}` : `hashrate dropped to ${formatHashrate(cur)}`, detail: `Δ${delta > 0 ? '+' : ''}${formatHashrate(delta)}`, ts: new Date(), }); } } }, [agents, push]); // ── Posture score change events ──────────────────────────────────────── useEffect(() => { for (const agent of agents) { if (agent.posture_score == null) continue; const prev = prevPosture.current[agent.id]; const cur = agent.posture_score; prevPosture.current[agent.id] = cur; if (prev === undefined) continue; const delta = cur - prev; if (Math.abs(delta) >= 10) { push({ id: eid(), kind: 'posture', agentId: agent.id, agentName: agent.name, message: `posture score ${delta > 0 ? 'improved' : 'degraded'} to ${cur}/100`, detail: `Δ${delta > 0 ? '+' : ''}${delta}`, ts: new Date(), }); } } }, [agents, push]); // ── New share events ─────────────────────────────────────────────────── const lastShareId = useRef(null); useEffect(() => { if (recentShares.length === 0) return; const top = recentShares[0]; const key = top.id != null ? String(top.id) : `${top.agent_id}-${top.hash}`; if (key === lastShareId.current) return; lastShareId.current = key; const name = agentMapRef.current.get(top.agent_id) ?? top.agent_id?.slice(0, 8); push({ id: eid(), kind: 'share', agentId: top.agent_id, agentName: name, message: top.accepted ? 'share accepted by pool' : 'share rejected', detail: top.accepted ? undefined : top.error ?? 'pool rejection', ts: new Date(top.timestamp ?? Date.now()), }); }, [recentShares, push]); // ── Fleet alert events ───────────────────────────────────────────────── const lastAlertId = useRef(null); useEffect(() => { if (fleetAlerts.length === 0) return; const top = fleetAlerts[0]; if (top.id === lastAlertId.current) return; lastAlertId.current = top.id; push({ id: eid(), kind: 'alert', agentId: top.agent_id, agentName: top.agent_name, message: top.message, detail: top.type, ts: new Date(top.timestamp ?? Date.now()), }); }, [fleetAlerts, push]); // ── Command result events ────────────────────────────────────────────── const lastCmdSeq = useRef(-1); useEffect(() => { if (commandResults.length === 0) return; const top = commandResults[commandResults.length - 1]; if ((top._seq ?? -1) <= lastCmdSeq.current) return; lastCmdSeq.current = top._seq ?? -1; const name = agentMapRef.current.get(top.agent_id ?? '') ?? top.agent_id?.slice(0, 8); push({ id: eid(), kind: 'command', agentId: top.agent_id, agentName: name, message: `${top.action} → ${top.success ? 'success' : 'failed'}`, detail: top.success ? undefined : top.message?.slice(0, 80), ts: new Date(), }); }, [commandResults, push]); // ── AI activity events ───────────────────────────────────────────────── const lastAiAgent = useRef>({}); useEffect(() => { for (const entry of aiActivity) { const lastAction = lastAiAgent.current[entry.agent_id]; if (entry.last_action && entry.last_action !== lastAction) { lastAiAgent.current[entry.agent_id] = entry.last_action; const name = agentMapRef.current.get(entry.agent_id) ?? entry.agent_id?.slice(0, 8); push({ id: eid(), kind: 'ai', agentId: entry.agent_id, agentName: name, message: `AI decided: ${entry.last_action}`, detail: entry.last_reasoning?.slice(0, 80), ts: entry.last_decide_at ? new Date(entry.last_decide_at) : new Date(), }); } } }, [aiActivity, push]); // ── Auto-scroll ──────────────────────────────────────────────────────── useEffect(() => { if (!autoScroll || !streamRef.current) return; streamRef.current.scrollTop = 0; // newest is at top }, [events, autoScroll]); // ── Filtered view ────────────────────────────────────────────────────── const filtered = useMemo(() => { let list = events.filter((e) => activeFilters.has(e.kind)); if (search.trim()) { const q = search.trim().toLowerCase(); list = list.filter((e) => (e.agentName ?? '').toLowerCase().includes(q) || e.message.toLowerCase().includes(q) || (e.detail ?? '').toLowerCase().includes(q) ); } return list; }, [events, activeFilters, search]); // ── Stats for pills ──────────────────────────────────────────────────── const onlineCount = agents.filter((a) => a.status === 'online').length; const totalHashrate = agents.reduce((s, a) => s + (a.hashrate_15m ?? 0), 0); const alertCount = fleetAlerts.length; const toggleFilter = (kind: ActivityEventKind) => { setActiveFilters((prev) => { const next = new Set(prev); if (next.has(kind)) { next.delete(kind); } else { next.add(kind); } if (next.size === 0) return new Set(ALL_KINDS); // prevent empty return next; }); }; const countByKind = useMemo(() => { const m: Record = {}; for (const e of events) m[e.kind] = (m[e.kind] ?? 0) + 1; return m; }, [events]); return (
{/* ── Hero ─────────────────────────────────────────────────────────── */}

REAL-TIME INTELLIGENCE

Activity Feed

Live event stream · agent connects · hashrate · shares · commands · AI decisions

{isConnected ? 'LIVE' : 'DISCONNECTED'} {isConnected && · {events.length} events}
{/* ── Stats pills ──────────────────────────────────────────────────── */}
{onlineCount} / {agents.length} online
{totalHashrate > 0 && (
{formatHashrate(totalHashrate)}
)} {alertCount > 0 && (
{alertCount} alert{alertCount !== 1 ? 's' : ''}
)}
setAutoScroll(e.target.checked)} style={{ cursor: 'pointer', accentColor: '#00e8f5' }} />
{/* ── Filter bar ───────────────────────────────────────────────────── */}
FILTER: {ALL_KINDS.map((kind) => { const cfg = KIND_CONFIG[kind]; const isActive = activeFilters.has(kind); const count = countByKind[kind] ?? 0; return ( ); })} setSearch(e.target.value)} /> {(events.length > 0 || search) && ( )}
{/* ── Event stream ─────────────────────────────────────────────────── */}
◆ LIVE EVENT STREAM sorted by most recent {filtered.length} events{search ? ' matching' : ''}
{ // Disable auto-scroll when user scrolls away from top const el = e.currentTarget; setAutoScroll(el.scrollTop < 60); }} > {filtered.length === 0 ? (
📡 {events.length === 0 ? 'Waiting for fleet events…' : 'No events match your filters.'}
) : ( filtered.map((ev) => ) )}
); }