Add fleet groups, agent screenshots, deploy guards, and Crucible polish.

This commit is contained in:
AetherForge
2026-06-02 20:51:52 -07:00
parent 01d76b3730
commit 41b5ec7a88
66 changed files with 1773 additions and 388 deletions

View File

@@ -16,6 +16,11 @@ import {
formatUptime,
} from '../help/fleetFilters';
import type { FleetFilterState } from '../help/fleetFilters';
import { downloadScreenshotFromBase64, sanitizeScreenshotBase64 } from '../help/screenshotDownload';
import { groupsForAgent } from '../help/fleetGroups';
import { useFleetGroups } from '../hooks/useFleetGroups';
import CreateGroupModal from '../components/Fleet/CreateGroupModal';
import FleetGroupsStrip from '../components/Fleet/FleetGroupsStrip';
import '../components/Fleet/FleetToolbar.css';
import './Pages.css';
@@ -96,6 +101,34 @@ export default function AgentsPage() {
const [metaMsg, setMetaMsg] = useState('');
const isConnectedRef = useRef(isConnected);
isConnectedRef.current = isConnected;
const screenshotWatchId = useRef<string | null>(null);
const screenshotSeqRef = useRef(0);
const [showGroupModal, setShowGroupModal] = useState(false);
const { groups, addGroup, removeGroup } = useFleetGroups();
const onlineAgentIds = useMemo(
() => new Set(agents.filter((a) => a.status === 'online').map((a) => a.id)),
[agents]
);
useEffect(() => {
if (!commandResults?.length || !screenshotWatchId.current) return;
const watch = screenshotWatchId.current;
for (const r of commandResults) {
if (r._seq <= screenshotSeqRef.current) continue;
if (r.agent_id !== watch || r.action !== 'screenshot') continue;
screenshotSeqRef.current = r._seq;
screenshotWatchId.current = null;
const label = agents.find((a) => a.id === watch)?.name ?? watch.slice(0, 8);
if (r.success && r.message) {
const ok = downloadScreenshotFromBase64(sanitizeScreenshotBase64(r.message), label);
if (!ok) alert(`Screenshot from ${label} failed — empty or invalid image.`);
} else {
alert(`Screenshot failed on ${label}: ${r.message ?? 'unknown error'}`);
}
break;
}
}, [commandResults, agents]);
useEffect(() => {
let cancelled = false;
@@ -280,6 +313,33 @@ export default function AgentsPage() {
return;
}
if (action === 'screenshot') {
if (onlineIds.length !== 1) {
alert('Select exactly one online machine (checkbox) for screenshot.');
return;
}
const id = onlineIds[0];
const label = agents.find((a) => a.id === id)?.name ?? 'agent';
screenshotWatchId.current = id;
if (commandResults?.length) {
screenshotSeqRef.current = commandResults[commandResults.length - 1]._seq;
}
setBulkBusy(true);
try {
const res = await api.sendAgentCommand(id, 'screenshot');
if (res.success === false) {
screenshotWatchId.current = null;
alert(res.error ?? 'Screenshot command rejected');
}
} catch (err) {
screenshotWatchId.current = null;
alert(err instanceof Error ? err.message : 'Screenshot failed');
} finally {
setBulkBusy(false);
}
return;
}
if (action === 'stop' && !window.confirm(`Stop miner on ${onlineIds.length} agent(s)?`)) return;
setBulkBusy(true);
@@ -333,8 +393,17 @@ export default function AgentsPage() {
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={(g) => setSelectedIds(new Set(g.agentIds))}
onDeleteGroup={removeGroup}
onCreateGroup={() => setShowGroupModal(true)}
/>
<div className="agents-list">
{filteredAgents.map((agent) => (
<AgentListItem
@@ -348,6 +417,7 @@ export default function AgentsPage() {
onSelect={() => void selectAgent(agent)}
onToggleExpand={() => setExpandedId((prev) => (prev === agent.id ? null : agent.id))}
commandResults={commandResults}
memberGroups={groupsForAgent(groups, agent.id)}
/>
))}
{filteredAgents.length === 0 && (
@@ -551,6 +621,16 @@ export default function AgentsPage() {
)}
</div>
)}
<CreateGroupModal
open={showGroupModal}
agentCount={selectedIds.size}
onClose={() => setShowGroupModal(false)}
onCreate={(name, color) => {
addGroup(name, color, [...selectedIds]);
setShowGroupModal(false);
}}
/>
<footer style={{ marginTop: '3rem', paddingTop: '1rem', borderTop: '1px solid #333', textAlign: 'center', color: '#ff4444', fontSize: '0.85rem', fontFamily: 'monospace' }}>
DISCLAIMER: Use only on personal machines on your own network. Anything else is a crime.
</footer>