Add Command Center v2: DQ/RAG integration, S3 browser, Jupyter, GPU matrix.
Mirror mo/atc-GPU layout with config/, docs/, scripts/ for Gitea deploy.
This commit is contained in:
+296
@@ -0,0 +1,296 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { ActivityFeed } from './components/ActivityFeed'
|
||||
import { AgentRoster } from './components/AgentRoster'
|
||||
import { AgentTerminalGrid } from './components/AgentTerminalGrid'
|
||||
import { ChatPanel } from './components/ChatPanel'
|
||||
import { CommandDock } from './components/CommandDock'
|
||||
import { GpuPanel } from './components/GpuPanel'
|
||||
import { AmbientBackground } from './components/AmbientBackground'
|
||||
import { LiveClusterMap } from './components/LiveClusterMap'
|
||||
import { LiveDomainGrid } from './components/LiveDomainGrid'
|
||||
import { ThemeToggle } from './components/ThemeToggle'
|
||||
import type { Agent, AgentAnim, Approval, ChatMessage, FeedEntry, GpuStatus, StatusData, TerminalLine, WorkloadData } from './types'
|
||||
|
||||
const TABS = ['Overview', 'Terminals', 'Activity', 'Approvals', 'GPU'] as const
|
||||
type Tab = (typeof TABS)[number]
|
||||
|
||||
function wsUrl() {
|
||||
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws'
|
||||
return `${proto}://${window.location.host}/api/ws/ops`
|
||||
}
|
||||
|
||||
function LiveClock() {
|
||||
const [now, setNow] = useState(new Date())
|
||||
useEffect(() => {
|
||||
const t = setInterval(() => setNow(new Date()), 1000)
|
||||
return () => clearInterval(t)
|
||||
}, [])
|
||||
return (
|
||||
<span className="text-xs font-mono text-[var(--text-faint)] hidden lg:inline tabular-nums">
|
||||
{now.toLocaleTimeString()}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [tab, setTab] = useState<Tab>('Overview')
|
||||
const [agents, setAgents] = useState<Agent[]>([])
|
||||
const [status, setStatus] = useState<StatusData | null>(null)
|
||||
const [workload, setWorkload] = useState<WorkloadData | null>(null)
|
||||
const [gpu, setGpu] = useState<GpuStatus | null>(null)
|
||||
const [feed, setFeed] = useState<FeedEntry[]>([])
|
||||
const [approvals, setApprovals] = useState<Approval[]>([])
|
||||
const [chat, setChat] = useState<ChatMessage[]>([])
|
||||
const [anims, setAnims] = useState<Record<string, AgentAnim>>({})
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [terminals, setTerminals] = useState<Record<string, TerminalLine[]>>({})
|
||||
const [terminalLayout, setTerminalLayout] = useState<'grid' | 'focus'>('grid')
|
||||
|
||||
const appendTerminal = useCallback((line: TerminalLine) => {
|
||||
setTerminals((prev) => {
|
||||
const cur = prev[line.agent_id] || []
|
||||
return { ...prev, [line.agent_id]: [...cur, line].slice(-300) }
|
||||
})
|
||||
}, [])
|
||||
|
||||
const selectedAgent = useMemo(
|
||||
() => agents.find((a) => a.id === selectedId) || null,
|
||||
[agents, selectedId],
|
||||
)
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const [a, s, f, ap, g, t, w] = await Promise.all([
|
||||
fetch('/api/agents').then((r) => r.json()),
|
||||
fetch('/api/status').then((r) => r.json()),
|
||||
fetch('/api/feed').then((r) => r.json()),
|
||||
fetch('/api/approvals').then((r) => r.json()),
|
||||
fetch('/api/gpu').then((r) => r.json()).catch(() => null),
|
||||
fetch('/api/terminals').then((r) => r.json()).catch(() => ({ terminals: {} })),
|
||||
fetch('/api/workload').then((r) => r.json()).catch(() => null),
|
||||
])
|
||||
setAgents(a.agents || [])
|
||||
setStatus(s)
|
||||
setGpu(g || s.gpu || null)
|
||||
setFeed(f.entries || [])
|
||||
setApprovals(ap.approvals || [])
|
||||
if (t.terminals) setTerminals(t.terminals)
|
||||
if (w?.zones) setWorkload(w)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
const ws = new WebSocket(wsUrl())
|
||||
ws.onmessage = (ev) => {
|
||||
const msg = JSON.parse(ev.data)
|
||||
if (msg.type === 'status') {
|
||||
setStatus(msg.data)
|
||||
if (msg.data.gpu) setGpu(msg.data.gpu)
|
||||
}
|
||||
if (msg.type === 'workload') setWorkload(msg.data)
|
||||
if (msg.type === 'terminal') appendTerminal(msg.line)
|
||||
if (msg.type === 'terminal_history' && msg.terminals) setTerminals(msg.terminals)
|
||||
if (msg.type === 'feed') setFeed((prev) => [msg.entry, ...prev].slice(0, 100))
|
||||
if (msg.type === 'agent_dispatch') {
|
||||
setSelectedId(msg.agent_id)
|
||||
setAnims((p) => ({ ...p, [msg.agent_id]: { agentId: msg.agent_id, state: 'walk', zone: msg.zone } }))
|
||||
}
|
||||
if (msg.type === 'agent_fetch') {
|
||||
setAnims((p) => ({ ...p, [msg.agent_id]: { agentId: msg.agent_id, state: 'fetch', zone: msg.zone } }))
|
||||
}
|
||||
if (msg.type === 'agent_return') {
|
||||
setAnims((p) => ({ ...p, [msg.agent_id]: { agentId: msg.agent_id, state: 'return', zone: msg.zone } }))
|
||||
setTimeout(() => {
|
||||
setAnims((p) => ({ ...p, [msg.agent_id]: { agentId: msg.agent_id, state: 'idle' } }))
|
||||
}, 1200)
|
||||
}
|
||||
if (msg.type === 'prompt_result') {
|
||||
setChat((c) => [...c, { role: 'agent', text: msg.answer, agent: msg.agent_id, ts: new Date().toISOString() }])
|
||||
setBusy(false)
|
||||
load()
|
||||
}
|
||||
}
|
||||
const iv = setInterval(load, 15000)
|
||||
return () => { ws.close(); clearInterval(iv) }
|
||||
}, [load, appendTerminal])
|
||||
|
||||
const sendPrompt = async (message: string, agentId?: string) => {
|
||||
setBusy(true)
|
||||
setChat((c) => [...c, { role: 'user', text: message, ts: new Date().toISOString() }])
|
||||
if (agentId) setSelectedId(agentId)
|
||||
await fetch('/api/prompt', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message, agent_id: agentId || undefined }),
|
||||
})
|
||||
}
|
||||
|
||||
const decide = async (id: string, approved: boolean) => {
|
||||
await fetch(`/api/approvals/${id}/decide`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ approved }),
|
||||
})
|
||||
load()
|
||||
}
|
||||
|
||||
const allOk = status && Object.values(status.domains).every((d) => d.level === 'ok')
|
||||
const totalTasks = agents.reduce((n, a) => n + (a.stats?.tasks || 0), 0)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen p-4 md:p-6 max-w-[96rem] mx-auto font-display flex flex-col gap-5 relative">
|
||||
<AmbientBackground />
|
||||
<header className="panel rounded-2xl px-5 py-4 flex flex-wrap justify-between items-center gap-4 relative overflow-hidden">
|
||||
<div className="header-aurora" />
|
||||
<div className="flex items-center gap-4 relative z-10">
|
||||
<div className="logo-mark">ATC</div>
|
||||
<div>
|
||||
<h1 className="text-2xl md:text-3xl font-bold tracking-tight neon-text" style={{ color: 'var(--accent)' }}>
|
||||
Command Center
|
||||
</h1>
|
||||
<p className="text-xs font-mono text-[var(--text-muted)]">
|
||||
{agents.length} agents · {totalTasks} missions · autonomous ops
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 flex-wrap relative z-10">
|
||||
<LiveClock />
|
||||
{status && (
|
||||
<span className={`status-pill ${allOk ? 'ok' : 'warn'}`}>
|
||||
{allOk ? 'All systems operational' : 'Attention required'}
|
||||
</span>
|
||||
)}
|
||||
{gpu?.ok && gpu.active_model && (
|
||||
<span className="status-pill gpu hidden md:inline-flex">
|
||||
GPU · {gpu.active_model}
|
||||
</span>
|
||||
)}
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<AgentRoster
|
||||
agents={agents}
|
||||
animations={anims}
|
||||
selectedId={selectedId}
|
||||
onSelect={setSelectedId}
|
||||
onDelegate={(a) => setSelectedId(a.id)}
|
||||
/>
|
||||
|
||||
<section className="panel rounded-2xl p-5">
|
||||
<div className="flex flex-wrap items-end justify-between gap-3 mb-4">
|
||||
<div>
|
||||
<p className="section-eyebrow">Live agent shells</p>
|
||||
<h2 className="text-lg font-bold" style={{ color: 'var(--accent)' }}>Agent Terminals</h2>
|
||||
<p className="text-xs text-[var(--text-muted)] mt-1">Volg live hoe elke agent data ophaalt — HTTP probes, Dockhand, JMX, vLLM.</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button type="button" className={`tab-btn text-xs ${terminalLayout === 'grid' ? 'active' : ''}`} onClick={() => setTerminalLayout('grid')}>Grid</button>
|
||||
<button type="button" className={`tab-btn text-xs ${terminalLayout === 'focus' ? 'active' : ''}`} onClick={() => setTerminalLayout('focus')}>Focus</button>
|
||||
</div>
|
||||
</div>
|
||||
<AgentTerminalGrid
|
||||
agents={agents}
|
||||
terminals={terminals}
|
||||
animations={anims}
|
||||
selectedId={selectedId}
|
||||
onSelect={setSelectedId}
|
||||
layout={terminalLayout}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<div className="grid grid-cols-1 xl:grid-cols-12 gap-5">
|
||||
<div className="xl:col-span-8 flex flex-col gap-5">
|
||||
<LiveClusterMap agents={agents} workload={workload} animations={anims} selectedId={selectedId} />
|
||||
|
||||
<nav className="flex gap-2 flex-wrap">
|
||||
{TABS.map((t) => (
|
||||
<button key={t} type="button" onClick={() => setTab(t)} className={`tab-btn ${tab === t ? 'active' : ''}`}>
|
||||
{t}
|
||||
{t === 'Approvals' && approvals.length > 0 && (
|
||||
<span className="tab-badge">{approvals.length}</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<main className="panel rounded-2xl p-5 min-h-[280px]">
|
||||
{tab === 'Overview' && (
|
||||
<div>
|
||||
<p className="section-eyebrow mb-1">Infrastructure</p>
|
||||
<h3 className="text-lg font-bold mb-4" style={{ color: 'var(--text)' }}>Live Cluster Workload</h3>
|
||||
<LiveDomainGrid workload={workload} />
|
||||
</div>
|
||||
)}
|
||||
{tab === 'Terminals' && (
|
||||
<div>
|
||||
<p className="section-eyebrow mb-1">Mission trace</p>
|
||||
<h3 className="text-lg font-bold mb-4" style={{ color: 'var(--text)' }}>
|
||||
{selectedAgent ? `${selectedAgent.name} — full terminal` : 'Select an agent'}
|
||||
</h3>
|
||||
{selectedAgent ? (
|
||||
<AgentTerminalGrid
|
||||
agents={agents}
|
||||
terminals={terminals}
|
||||
animations={anims}
|
||||
selectedId={selectedId}
|
||||
onSelect={setSelectedId}
|
||||
layout="focus"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-sm text-[var(--text-muted)]">Klik een agent in de roster of stuur een prompt.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{tab === 'Activity' && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<p className="section-eyebrow">Event stream</p>
|
||||
<h3 className="text-lg font-bold" style={{ color: 'var(--text)' }}>Agent Activity</h3>
|
||||
</div>
|
||||
{selectedAgent && (
|
||||
<button type="button" className="btn-secondary text-xs" onClick={() => setSelectedId(null)}>
|
||||
Clear filter
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<ActivityFeed feed={feed} agents={agents} filterAgentId={selectedId} />
|
||||
</div>
|
||||
)}
|
||||
{tab === 'Approvals' && (
|
||||
<div className="space-y-3">
|
||||
{approvals.length === 0 && <p className="text-[var(--text-muted)] text-sm">Geen pending approvals.</p>}
|
||||
{approvals.map((a) => {
|
||||
const agent = agents.find((ag) => ag.id === a.agent_id)
|
||||
return (
|
||||
<div key={a.id} className="status-card rounded-xl p-4 flex gap-4" style={{ borderColor: 'var(--accent-secondary)' }}>
|
||||
{agent && <span className="text-2xl">{agent.icon}</span>}
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-semibold" style={{ color: 'var(--accent-secondary)' }}>{a.action}</div>
|
||||
<div className="text-xs text-[var(--text-muted)] mt-1">{a.reason}</div>
|
||||
<div className="text-[10px] font-mono text-[var(--text-faint)] mt-1">via {agent?.name || a.agent_id}</div>
|
||||
<div className="flex gap-2 mt-3">
|
||||
<button type="button" onClick={() => decide(a.id, true)} className="btn-secondary text-[var(--status-ok)]">Approve</button>
|
||||
<button type="button" onClick={() => decide(a.id, false)} className="btn-secondary text-[var(--status-down)]">Deny</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{tab === 'GPU' && <GpuPanel gpu={gpu} />}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div className="xl:col-span-4 flex flex-col gap-5">
|
||||
<GpuPanel gpu={gpu} compact />
|
||||
<ChatPanel messages={chat} agents={agents} selectedAgent={selectedAgent} busy={busy} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CommandDock onSubmit={sendPrompt} busy={busy} selectedAgent={selectedAgent} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user