Improve fleet control, Crucible ops, and multi-machine identity.
Use hostname-first agent names so the same forged binary on many machines stays distinct at scale. Add WebSocket RTT latency on the roster and Crucible, fleet delete and uninstall flows, live alert config reload, and non-blocking pool setup. Fix Crucible phantom agents after delete, posture scan targeting, and USB portability (config data_dir, LAUNCH sync).
This commit is contained in:
@@ -2,6 +2,7 @@ import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||
import { api } from '../api/client';
|
||||
import { useWebSocket } from '../hooks/useWebSocket';
|
||||
import type { Agent, HashrateSample, ServerInfo } from '../types';
|
||||
import LatencyBadge from '../components/Fleet/LatencyBadge';
|
||||
import HashrateChart from '../components/Charts/HashrateChart';
|
||||
import NeonCard from '../components/NeonCard/NeonCard';
|
||||
import AgentRemoteActions from '../components/Fleet/AgentRemoteActions';
|
||||
@@ -139,9 +140,19 @@ export default function AgentsPage() {
|
||||
}
|
||||
}, [selectedAgent?.id, agentLogs]);
|
||||
|
||||
// Sort: online first, then by last_seen desc, then alphabetical
|
||||
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(agents, filters),
|
||||
[agents, filters]
|
||||
() => filterFleetAgents(sortedAgents, filters),
|
||||
[sortedAgents, filters]
|
||||
);
|
||||
|
||||
const refreshLog = async (refresh = false) => {
|
||||
@@ -199,10 +210,60 @@ export default function AgentsPage() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleDeleteAgent = async (agentId: string) => {
|
||||
if (!window.confirm('Remove this machine from the fleet roster? This cannot be undone.')) return;
|
||||
try {
|
||||
await api.deleteAgent(agentId);
|
||||
setAgents((prev) => prev.filter((a) => a.id !== agentId));
|
||||
if (selectedAgent?.id === agentId) setSelectedAgent(null);
|
||||
setSelectedIds((prev) => { const next = new Set(prev); next.delete(agentId); return next; });
|
||||
} catch (err) {
|
||||
alert(err instanceof Error ? err.message : 'Delete failed');
|
||||
}
|
||||
};
|
||||
|
||||
const handleUninstallAndDelete = async (agent: Agent) => {
|
||||
const label = agent.status === 'online'
|
||||
? `Uninstall the miner from "${agent.name}" and remove it from the roster?`
|
||||
: `"${agent.name}" is offline — it cannot be remotely uninstalled. Remove from roster only?`;
|
||||
if (!window.confirm(label)) return;
|
||||
if (agent.status === 'online') {
|
||||
try {
|
||||
await api.sendAgentCommand(agent.id, 'uninstall', {});
|
||||
} catch {
|
||||
// Non-fatal — proceed to delete the record regardless
|
||||
}
|
||||
}
|
||||
try {
|
||||
await api.deleteAgent(agent.id);
|
||||
setAgents((prev) => prev.filter((a) => a.id !== agent.id));
|
||||
if (selectedAgent?.id === agent.id) setSelectedAgent(null);
|
||||
setSelectedIds((prev) => { const next = new Set(prev); next.delete(agent.id); return next; });
|
||||
} catch (err) {
|
||||
alert(err instanceof Error ? err.message : 'Delete failed');
|
||||
}
|
||||
};
|
||||
|
||||
const handleBulkAction = async (action: string) => {
|
||||
const ids = [...selectedIds];
|
||||
if (ids.length === 0) return;
|
||||
|
||||
if (action === 'delete') {
|
||||
if (!window.confirm(`Permanently remove ${ids.length} machine(s) from the fleet roster?`)) return;
|
||||
setBulkBusy(true);
|
||||
try {
|
||||
await api.bulkDeleteAgents(ids);
|
||||
setAgents((prev) => prev.filter((a) => !ids.includes(a.id)));
|
||||
if (selectedAgent && ids.includes(selectedAgent.id)) setSelectedAgent(null);
|
||||
setSelectedIds(new Set());
|
||||
} catch (err) {
|
||||
alert(err instanceof Error ? err.message : 'Bulk delete failed');
|
||||
} finally {
|
||||
setBulkBusy(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let targetIds = ids;
|
||||
if (action === 'restart_idle') {
|
||||
targetIds = agents.filter((a) => ids.includes(a.id) && agentIsIdleMiner(a)).map((a) => a.id);
|
||||
@@ -323,16 +384,57 @@ export default function AgentsPage() {
|
||||
value={tagsDraft}
|
||||
onChange={(e) => setTagsDraft(e.target.value)}
|
||||
/>
|
||||
<button type="button" className="btn btn-outline btn-sm" disabled={metaSaving} onClick={() => void saveMeta()}>
|
||||
{metaSaving ? 'Saving…' : 'Save notes & tags'}
|
||||
</button>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<button type="button" className="btn btn-outline btn-sm" disabled={metaSaving} onClick={() => void saveMeta()}>
|
||||
{metaSaving ? 'Saving…' : 'Save notes & tags'}
|
||||
</button>
|
||||
{selectedAgent.status === 'online' && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm"
|
||||
style={{ background: 'rgba(255,100,0,0.15)', border: '1px solid #ff8844', color: '#ffaa66' }}
|
||||
onClick={() => void handleUninstallAndDelete(selectedAgent)}
|
||||
title="Send uninstall command to agent, then remove from roster"
|
||||
>
|
||||
⚡ Uninstall + Delete
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm"
|
||||
style={{ background: 'rgba(255,40,40,0.15)', border: '1px solid #ff4444', color: '#ff6666' }}
|
||||
onClick={() => void handleDeleteAgent(selectedAgent.id)}
|
||||
title="Remove this machine from the fleet roster permanently"
|
||||
>
|
||||
🗑 Delete from Roster
|
||||
</button>
|
||||
</div>
|
||||
{metaMsg && <span className="form-hint">{metaMsg}</span>}
|
||||
</div>
|
||||
|
||||
<div className="agent-detail-grid">
|
||||
<div className="detail-item">
|
||||
<span className="detail-label">Status</span>
|
||||
<span className={`status-badge ${selectedAgent.status}`}>{selectedAgent.status}</span>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<span className={`status-badge ${selectedAgent.status}`}>{selectedAgent.status}</span>
|
||||
{selectedAgent.status === 'online' && (
|
||||
<LatencyBadge ms={selectedAgent.latency_ms} />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{selectedAgent.hostname && selectedAgent.hostname !== selectedAgent.name && (
|
||||
<div className="detail-item">
|
||||
<span className="detail-label">Hostname</span>
|
||||
<span className="detail-value mono">{selectedAgent.hostname}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="detail-item">
|
||||
<span className="detail-label">Last Seen</span>
|
||||
<span className="detail-value" title={selectedAgent.last_seen}>
|
||||
{selectedAgent.last_seen
|
||||
? new Date(selectedAgent.last_seen).toLocaleString()
|
||||
: '—'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="detail-item">
|
||||
<span className="detail-label">Wallet</span>
|
||||
|
||||
Reference in New Issue
Block a user