feat(ui): Setup tab � health check + bootstrap with live log

This commit is contained in:
2026-04-26 01:50:01 +00:00
parent 60eeb4d0ea
commit 4bf98c4d2b

View File

@@ -0,0 +1,189 @@
import React, { useState, useEffect, useCallback, useRef } from 'react'
import axios from 'axios'
import { CheckCircle2, XCircle, Loader2, Play, RefreshCw, Server, Cpu, Box, Folder, Wrench, Sparkles } from 'lucide-react'
const CHECK_LABELS = {
conda: { label: 'Miniconda installed', icon: Box },
env: { label: 'Conda env "synthetic-data"', icon: Box },
sdk: { label: 'synthetic-data-kit CLI', icon: Wrench },
training_deps: { label: 'Training libs (torch/peft/trl)', icon: Sparkles },
train_py: { label: 'train.py installed', icon: Wrench },
data_dirs: { label: 'Data directories created', icon: Folder },
gpu: { label: 'NVIDIA GPU detected', icon: Cpu },
}
const CHECK_ORDER = ['gpu', 'conda', 'env', 'sdk', 'training_deps', 'train_py', 'data_dirs']
export default function SetupPanel({ connected }) {
const [checks, setChecks] = useState(null)
const [ready, setReady] = useState(false)
const [user, setUser] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [running, setRunning] = useState(false)
const [logs, setLogs] = useState([])
const [stage, setStage] = useState('')
const logRef = useRef(null)
const runCheck = useCallback(async () => {
if (!connected) return
setLoading(true); setError('')
try {
const { data } = await axios.get('/api/setup/check')
setChecks(data.checks); setReady(data.ready); setUser(data.user)
} catch (e) {
setError(e.response?.data?.detail || e.message)
} finally {
setLoading(false)
}
}, [connected])
useEffect(() => { runCheck() }, [runCheck])
useEffect(() => {
if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight
}, [logs])
const runBootstrap = () => {
if (!connected || running) return
setRunning(true); setLogs([]); setStage('starting'); setError('')
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'
const ws = new WebSocket(`${proto}//${location.host}/api/setup/bootstrap`)
ws.onmessage = (e) => {
const msg = JSON.parse(e.data)
if (msg.type === 'log') {
const line = msg.data
const stageMatch = line.match(/::stage:: (\w+)/)
if (stageMatch) setStage(stageMatch[1])
setLogs(l => [...l, line])
} else if (msg.type === 'error') {
setError(msg.data)
setRunning(false)
ws.close()
} else if (msg.type === 'done') {
setStage('done')
setRunning(false)
ws.close()
runCheck()
}
}
ws.onerror = () => { setError('WebSocket error'); setRunning(false) }
ws.onclose = () => setRunning(false)
}
if (!connected) {
return (
<div className="rounded-xl border border-slate-700/50 bg-[#161b27] p-6 text-center">
<Server size={32} className="mx-auto mb-3 text-slate-600" />
<p className="text-sm text-slate-400">Connect to the GPU server first to run setup.</p>
</div>
)
}
return (
<div className="space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-base font-semibold text-slate-200">Remote setup</h2>
<p className="text-xs text-slate-500 mt-0.5">
Verify and install everything llm-trainer needs on the GPU host
{user && <> connected as <span className="text-slate-400">{user}</span></>}
</p>
</div>
<button
onClick={runCheck}
disabled={loading || running}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg border border-slate-700 hover:bg-slate-700/30 disabled:opacity-50"
>
<RefreshCw size={12} className={loading ? 'animate-spin' : ''} /> Re-check
</button>
</div>
{/* Health check grid */}
<div className="rounded-xl border border-slate-700/50 bg-[#161b27] overflow-hidden">
<div className="px-4 py-3 border-b border-slate-700/50 flex items-center justify-between">
<span className="text-xs text-slate-400">Health check</span>
{ready && (
<span className="text-xs text-green-400 flex items-center gap-1">
<CheckCircle2 size={12}/> Ready to train
</span>
)}
</div>
<div className="divide-y divide-slate-700/20">
{checks
? CHECK_ORDER.map(key => {
const meta = CHECK_LABELS[key]
const ok = checks[key]
const Icon = meta.icon
return (
<div key={key} className="flex items-center gap-3 px-4 py-2.5 text-xs">
<Icon size={14} className="text-slate-500" />
<span className="flex-1 text-slate-300">{meta.label}</span>
{ok
? <span className="flex items-center gap-1 text-green-400"><CheckCircle2 size={13}/> ok</span>
: <span className="flex items-center gap-1 text-red-400"><XCircle size={13}/> missing</span>
}
</div>
)
})
: <div className="px-4 py-6 text-xs text-slate-500 text-center">
{loading ? 'Probing remote…' : 'No data'}
</div>
}
</div>
</div>
{/* Bootstrap action */}
{checks && !ready && (
<div className="rounded-xl border border-blue-700/30 bg-blue-900/10 p-4">
<div className="flex items-start gap-3">
<div className="flex-1">
<h3 className="text-sm font-medium text-slate-200 mb-1">Bootstrap remote</h3>
<p className="text-xs text-slate-400">
Installs miniconda, creates the conda env, installs synthetic-data-kit + torch + transformers + peft + trl,
creates the data directories, and drops the training script into place. This can take 510 minutes
on first run depending on network speed.
</p>
</div>
<button
onClick={runBootstrap}
disabled={running}
className="flex items-center gap-2 px-4 py-2 text-xs font-medium rounded-lg bg-blue-600 hover:bg-blue-500 text-white disabled:opacity-50"
>
{running
? <><Loader2 size={13} className="animate-spin"/> Running ({stage})</>
: <><Play size={13}/> Bootstrap</>
}
</button>
</div>
</div>
)}
{/* Error */}
{error && (
<div className="rounded-xl border border-red-700/40 bg-red-900/10 p-3 text-xs text-red-300">
{error}
</div>
)}
{/* Live log */}
{logs.length > 0 && (
<div className="rounded-xl border border-slate-700/50 bg-[#0b0f17] overflow-hidden">
<div className="px-4 py-2 border-b border-slate-700/50 flex items-center justify-between">
<span className="text-xs text-slate-400">Bootstrap log</span>
{running && <Loader2 size={12} className="animate-spin text-blue-400" />}
</div>
<pre
ref={logRef}
className="max-h-[420px] overflow-auto p-3 text-xs leading-relaxed text-slate-300 font-mono whitespace-pre-wrap"
>
{logs.join('')}
</pre>
</div>
)}
</div>
)
}