import React, { useEffect, useRef, useState } from 'react'; import api from '../utils/api'; import EmptyState from './EmptyState/EmptyState'; import Modal from './Modal/Modal'; import './Dashboard.css'; import './Swarm.css'; import './DeckyFleet.css'; import { AlertTriangle, Check, Copy, HardDrive, PowerOff, RefreshCw, RotateCcw, Server, Trash2, UserPlus, Wifi, WifiOff, } from '../icons'; interface SwarmHost { uuid: string; name: string; address: string; agent_port: number; status: string; last_heartbeat: string | null; client_cert_fingerprint: string; updater_cert_fingerprint: string | null; enrolled_at: string; notes: string | null; } interface BundleResult { token: string; host_uuid: string; command: string; expires_at: string; } const shortFp = (fp: string): string => (fp ? fp.slice(0, 16) + '…' : '—'); // ─── Enrollment wizard ──────────────────────────────────────────────────── interface EnrollmentWizardProps { open: boolean; onClose: () => void; onEnrolled: () => void; } const EnrollmentWizard: React.FC = ({ open, onClose, onEnrolled }) => { const [step, setStep] = useState(0); const [masterHost, setMasterHost] = useState(window.location.hostname); const [agentName, setAgentName] = useState(''); const [withUpdater, setWithUpdater] = useState(true); const [useIpvlan, setUseIpvlan] = useState(false); const [servicesIni, setServicesIni] = useState(null); const [servicesIniName, setServicesIniName] = useState(null); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const [result, setResult] = useState(null); const [copied, setCopied] = useState(false); const [now, setNow] = useState(Date.now()); const fileRef = useRef(null); useEffect(() => { if (!open) return; setStep(0); setMasterHost(window.location.hostname); setAgentName(''); setWithUpdater(true); setUseIpvlan(false); setServicesIni(null); setServicesIniName(null); setSubmitting(false); setError(null); setResult(null); setCopied(false); if (fileRef.current) fileRef.current.value = ''; }, [open]); useEffect(() => { if (!result) return; const t = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(t); }, [result]); const handleFile = (e: React.ChangeEvent) => { const f = e.target.files?.[0]; if (!f) { setServicesIni(null); setServicesIniName(null); return; } const reader = new FileReader(); reader.onload = () => { setServicesIni(String(reader.result)); setServicesIniName(f.name); }; reader.readAsText(f); }; const nameOk = /^[a-z0-9][a-z0-9-]{0,62}$/.test(agentName); const generate = async () => { setSubmitting(true); setError(null); try { const res = await api.post('/swarm/enroll-bundle', { master_host: masterHost, agent_name: agentName, with_updater: withUpdater, use_ipvlan: useIpvlan, services_ini: servicesIni, }); setResult(res.data); onEnrolled(); } catch (err: unknown) { const e = err as { response?: { data?: { detail?: string } } }; setError(e?.response?.data?.detail || 'Enrollment bundle creation failed'); } finally { setSubmitting(false); } }; const copyCmd = async () => { if (!result) return; await navigator.clipboard.writeText(result.command); setCopied(true); setTimeout(() => setCopied(false), 2000); }; const remainingSecs = result ? Math.max(0, Math.floor((new Date(result.expires_at).getTime() - now) / 1000)) : 0; const mm = Math.floor(remainingSecs / 60).toString().padStart(2, '0'); const ss = (remainingSecs % 60).toString().padStart(2, '0'); const canNext = step === 0 ? (nameOk && !!masterHost) : true; return (
{step > 0 && !result && ( )} {step < 2 && ( )} {step === 2 && !result && ( )} {result && ( )}
} > <>
{['IDENTITY', 'OPTIONS', 'BUNDLE'].map((l, i) => (
{i + 1}. {l}
))}
{step === 0 && ( <>
Who is this worker, and how does it reach the master?
setMasterHost(e.target.value)} />
setAgentName(e.target.value.toLowerCase())} pattern="^[a-z0-9][a-z0-9-]{0,62}$" data-autofocus /> {agentName && !nameOk && ( must match ^[a-z0-9][a-z0-9-]{'{0,62}'}$ )}
)} {step === 1 && ( <>
Bundle options — tune for the target environment.
setWithUpdater(e.target.checked)} style={{ accentColor: 'var(--matrix)', marginTop: 2 }} />
setUseIpvlan(e.target.checked)} style={{ accentColor: 'var(--matrix)', marginTop: 2 }} />
{servicesIniName && (
loaded: {servicesIniName}
)}
)} {step === 2 && ( <> {!result ? ( <>
Review and generate a one-shot bootstrap URL valid for 5 minutes.
# enrollment bundle preview{'\n'} master_host{' '}{masterHost}{'\n'} agent_name {' '}{agentName}{'\n'} updater {' '}{withUpdater ? 'yes' : 'no'}{'\n'} ipvlan {' '}{useIpvlan ? 'yes' : 'no'}{'\n'} services {' '}{servicesIniName ?? '—'}
{error && (
✖ {error}
)} ) : ( <>
Paste this on the new worker (as root):
{result.command}
Expires in {mm}:{ss} — one-shot, single download. Host UUID: {result.host_uuid}
{remainingSecs === 0 && (
This bundle has expired. Generate another.
)} )} )}
); }; // ─── Swarm hosts page ───────────────────────────────────────────────────── const SwarmHosts: React.FC = () => { const [hosts, setHosts] = useState([]); const [loading, setLoading] = useState(true); const [decommissioning, setDecommissioning] = useState>(new Set()); const [tearingDown, setTearingDown] = useState>(new Set()); const [error, setError] = useState(null); const [showEnroll, setShowEnroll] = useState(false); // Two-click arm/commit replaces window.confirm(). Browsers silently // suppress confirm() after the "prevent additional dialogs" opt-out, // which manifests as a dead button — no network request, no console // error. Key format: ":". const [armed, setArmed] = useState(null); const arm = (key: string) => { setArmed(key); setTimeout(() => setArmed((prev) => (prev === key ? null : prev)), 4000); }; const fetchHosts = async () => { try { const res = await api.get('/swarm/hosts'); setHosts(res.data); setError(null); } catch (err: any) { setError(err?.response?.data?.detail || 'Failed to fetch swarm hosts'); } finally { setLoading(false); } }; useEffect(() => { fetchHosts(); const t = setInterval(fetchHosts, 10000); return () => clearInterval(t); }, []); const addTo = (set: Set, id: string) => { const n = new Set(set); n.add(id); return n; }; const removeFrom = (set: Set, id: string) => { const n = new Set(set); n.delete(id); return n; }; const handleTeardownAll = async (host: SwarmHost) => { const key = `teardown:${host.uuid}`; if (armed !== key) { arm(key); return; } setArmed(null); setTearingDown((s) => addTo(s, host.uuid)); try { // 202 Accepted — teardown runs async on the backend. await api.post(`/swarm/hosts/${host.uuid}/teardown`, {}); await fetchHosts(); } catch (err: any) { alert(err?.response?.data?.detail || 'Teardown failed'); } finally { setTearingDown((s) => removeFrom(s, host.uuid)); } }; const handleDecommission = async (host: SwarmHost) => { const key = `decom:${host.uuid}`; if (armed !== key) { arm(key); return; } setArmed(null); setDecommissioning((s) => addTo(s, host.uuid)); try { await api.delete(`/swarm/hosts/${host.uuid}`); await fetchHosts(); } catch (err: any) { alert(err?.response?.data?.detail || 'Decommission failed'); } finally { setDecommissioning((s) => removeFrom(s, host.uuid)); } }; const online = hosts.filter((h) => h.status === 'online').length; return (

SWARM HOSTS

{loading ? 'LOADING…' : `${hosts.length} ENROLLED · ${online} ONLINE`}
{error &&
{error}
}
{loading ? (

Loading hosts…

) : hosts.length === 0 ? ( setShowEnroll(true) }} /> ) : ( {hosts.map((h) => ( ))}
Status Name Address Last heartbeat Client cert Enrolled
{h.status === 'active' ? : } {h.status} {h.name} {h.address ? `${h.address}:${h.agent_port}` : pending first connect} {h.last_heartbeat ? new Date(h.last_heartbeat).toLocaleString() : '—'} {shortFp(h.client_cert_fingerprint)} {new Date(h.enrolled_at).toLocaleString()}
)}
setShowEnroll(false)} onEnrolled={fetchHosts} />
); }; export default SwarmHosts;