Add fleet groups, agent screenshots, deploy guards, and Crucible polish.
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user