import React, { useEffect, useState } from 'react'; import api from '../utils/api'; import EmptyState from './EmptyState/EmptyState'; import './Dashboard.css'; import { Upload, RefreshCw, RotateCcw, Package, AlertTriangle, CheckCircle, Wifi, WifiOff, Server, } from 'lucide-react'; interface HostRelease { host_uuid: string; host_name: string; address: string; reachable: boolean; agent_status?: string | null; current_sha?: string | null; previous_sha?: string | null; releases: Array>; detail?: string | null; } interface PushResult { host_uuid: string; host_name: string; status: 'updated' | 'rolled-back' | 'failed' | 'self-updated' | 'self-failed'; http_status?: number | null; sha?: string | null; detail?: string | null; stderr?: string | null; } interface Toast { id: number; kind: 'success' | 'warn' | 'error'; text: string; } const shortSha = (s: string | null | undefined): string => (s ? s.slice(0, 7) : '—'); const RemoteUpdates: React.FC = () => { const [hosts, setHosts] = useState([]); const [loading, setLoading] = useState(true); const [isAdmin, setIsAdmin] = useState(false); const [busyRow, setBusyRow] = useState(null); const [fleetBusy, setFleetBusy] = useState(false); const [showFleetModal, setShowFleetModal] = useState(false); const [includeSelf, setIncludeSelf] = useState(false); const [toasts, setToasts] = useState([]); const pushToast = (kind: Toast['kind'], text: string) => { const id = Date.now() + Math.random(); setToasts((t) => [...t, { id, kind, text }]); setTimeout(() => setToasts((t) => t.filter((x) => x.id !== id)), 7000); }; const fetchHosts = async () => { try { const res = await api.get('/swarm-updates/hosts'); setHosts(res.data.hosts || []); } catch (err: any) { if (err.response?.status !== 403) console.error('Failed to fetch host releases', err); } finally { setLoading(false); } }; const fetchRole = async () => { try { const res = await api.get('/config'); setIsAdmin(res.data.role === 'admin'); } catch { setIsAdmin(false); } }; useEffect(() => { fetchRole(); fetchHosts(); const interval = setInterval(fetchHosts, 10000); return () => clearInterval(interval); }, []); const describeResult = (r: PushResult): Toast => { const sha = shortSha(r.sha); switch (r.status) { case 'updated': return { id: 0, kind: 'success', text: `${r.host_name} → updated (sha ${sha})` }; case 'self-updated': return { id: 0, kind: 'success', text: `${r.host_name} → updater upgraded (sha ${sha})` }; case 'rolled-back': return { id: 0, kind: 'warn', text: `${r.host_name} → rolled back: ${r.detail || r.stderr || 'probe failed'}` }; case 'failed': return { id: 0, kind: 'error', text: `${r.host_name} → failed: ${r.detail || 'transport error'}` }; case 'self-failed': return { id: 0, kind: 'error', text: `${r.host_name} → updater push failed: ${r.detail || 'unknown'}` }; } }; const handlePush = async (host: HostRelease, kind: 'agent' | 'self') => { setBusyRow(host.host_uuid); const endpoint = kind === 'agent' ? '/swarm-updates/push' : '/swarm-updates/push-self'; try { const res = await api.post(endpoint, { host_uuids: [host.host_uuid] }, { timeout: 240000 }); (res.data.results as PushResult[]).forEach((r) => { const t = describeResult(r); pushToast(t.kind, t.text); }); await fetchHosts(); } catch (err: any) { pushToast('error', `${host.host_name} → request failed: ${err.response?.data?.detail || err.message}`); } finally { setBusyRow(null); } }; const handleRollback = async (host: HostRelease) => { if (!window.confirm(`Roll back ${host.host_name} to its previous release?`)) return; setBusyRow(host.host_uuid); try { const res = await api.post('/swarm-updates/rollback', { host_uuid: host.host_uuid }, { timeout: 60000 }); const r = res.data as PushResult & { status: 'rolled-back' | 'failed' }; if (r.status === 'rolled-back') { pushToast('success', `${host.host_name} → rolled back`); } else { pushToast('error', `${host.host_name} → rollback failed: ${r.detail || 'unknown'}`); } await fetchHosts(); } catch (err: any) { pushToast('error', `${host.host_name} → rollback failed: ${err.response?.data?.detail || err.message}`); } finally { setBusyRow(null); } }; const handleFleetPush = async () => { setFleetBusy(true); setShowFleetModal(false); try { const res = await api.post( '/swarm-updates/push', { all: true, include_self: includeSelf }, { timeout: 600000 }, ); (res.data.results as PushResult[]).forEach((r) => { const t = describeResult(r); pushToast(t.kind, t.text); }); await fetchHosts(); } catch (err: any) { pushToast('error', `Fleet push failed: ${err.response?.data?.detail || err.message}`); } finally { setFleetBusy(false); } }; if (loading) return
QUERYING WORKER UPDATER FLEET...
; if (!isAdmin) { return (
Admin role required for Remote Updates.
); } return (

REMOTE UPDATES

push updater bundles to enrolled workers · {hosts.length} WORKER{hosts.length === 1 ? '' : 'S'}
{showFleetModal && (

Push current tree to every enrolled worker

A tarball of the master's working tree will be uploaded to each worker's updater, installed, and the agent will be restarted. Failed probes auto-roll-back.

)} {hosts.length === 0 ? ( ) : (
{hosts.map((h) => { const busy = busyRow === h.host_uuid; return (
{h.reachable ? : } {h.host_name} {h.address}
{h.reachable ? (
) : (
UNREACHABLE — {h.detail || 'no response'}
)}
); })}
)}
{toasts.map((t) => (
{t.kind === 'success' ? : } {t.text}
))}
); }; const Info: React.FC<{ label: string; value: string; tone: 'accent' | 'dim' }> = ({ label, value, tone }) => (
{label} {value}
); export default RemoteUpdates;