Add tiered LOTL mining onion and fleet recon so agents can fallback across execution tiers while operators see spread and vuln posture in Crucible. Includes triple-onion chain, spread cred graph, and full Go/TS/E2E test validation.
This commit is contained in:
132
server/web/src/components/Fleet/CrucibleAgentMeta.tsx
Normal file
132
server/web/src/components/Fleet/CrucibleAgentMeta.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { api } from '../../api/client';
|
||||
import type { Agent } from '../../types';
|
||||
|
||||
interface Props {
|
||||
agent: Agent;
|
||||
onUpdated?: (agent: Agent) => void;
|
||||
}
|
||||
|
||||
export default function CrucibleAgentMeta({ agent, onUpdated }: Props) {
|
||||
const [notesDraft, setNotesDraft] = useState(agent.notes || '');
|
||||
const [tagsDraft, setTagsDraft] = useState((agent.tags || []).join(', '));
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [msg, setMsg] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setNotesDraft(agent.notes || '');
|
||||
setTagsDraft((agent.tags || []).join(', '));
|
||||
setMsg('');
|
||||
}, [agent.id, agent.notes, agent.tags]);
|
||||
|
||||
const save = async () => {
|
||||
setSaving(true);
|
||||
setMsg('');
|
||||
const tags = tagsDraft.split(',').map((t) => t.trim()).filter(Boolean);
|
||||
try {
|
||||
const res = await api.updateAgentMeta(agent.id, notesDraft, tags);
|
||||
onUpdated?.(res.agent);
|
||||
setMsg('Saved');
|
||||
setTimeout(() => setMsg(''), 2000);
|
||||
} catch (err) {
|
||||
setMsg(err instanceof Error ? err.message : 'Save failed');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteFromRoster = async () => {
|
||||
if (!window.confirm('Remove this machine from the fleet roster? This cannot be undone.')) return;
|
||||
try {
|
||||
await api.deleteAgent(agent.id);
|
||||
} catch (err) {
|
||||
alert(err instanceof Error ? err.message : 'Delete failed');
|
||||
}
|
||||
};
|
||||
|
||||
const uninstallAndDelete = async () => {
|
||||
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);
|
||||
} catch (err) {
|
||||
alert(err instanceof Error ? err.message : 'Delete failed');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="crucible-agent-meta" style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.5rem',
|
||||
marginBottom: '1rem',
|
||||
padding: '0.75rem 1rem',
|
||||
background: 'rgba(0,245,255,0.04)',
|
||||
border: '1px solid rgba(0,245,255,0.18)',
|
||||
borderRadius: '8px',
|
||||
}}>
|
||||
<div className="font-tech" style={{ fontSize: '0.72rem', letterSpacing: '0.1em', color: 'var(--neon-cyan)' }}>
|
||||
NOTES & TAGS
|
||||
</div>
|
||||
<p className="form-hint" style={{ margin: 0 }}>
|
||||
Labels like "Living room PC" or "Rack B" — stored on the server, shown on node cards.
|
||||
</p>
|
||||
{(agent.tags?.length ?? 0) > 0 && (
|
||||
<div>
|
||||
{agent.tags!.map((t) => (
|
||||
<span key={t} className="agent-tag-chip">{t}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<textarea
|
||||
className="input"
|
||||
rows={2}
|
||||
placeholder="Notes about this machine…"
|
||||
value={notesDraft}
|
||||
onChange={(e) => setNotesDraft(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
className="input mono agent-meta-tags-input"
|
||||
placeholder="Tags: living-room, rack-b (comma separated)"
|
||||
value={tagsDraft}
|
||||
onChange={(e) => setTagsDraft(e.target.value)}
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<button type="button" className="btn btn-outline btn-sm" disabled={saving} onClick={() => void save()}>
|
||||
{saving ? 'Saving…' : 'Save notes & tags'}
|
||||
</button>
|
||||
{agent.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 uninstallAndDelete()}
|
||||
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 deleteFromRoster()}
|
||||
title="Remove this machine from the fleet roster permanently"
|
||||
>
|
||||
Delete from Roster
|
||||
</button>
|
||||
{msg && <span className="form-hint">{msg}</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user