import { useEffect, useRef, useState } from 'react' import { Terminal as XTerm } from '@xterm/xterm' import { FitAddon } from '@xterm/addon-fit' import '@xterm/xterm/css/xterm.css' import { Play, Square, RotateCw, Pause, PlayCircle, Trash2, RefreshCw, TerminalSquare, ScrollText, X, } from 'lucide-react' import { api, getToken, type Container, type ContainerStats, type Integration } from '../lib/api' const TEXT_PRIMARY = '#E8E6E0' const TEXT_SECONDARY = '#7A7D85' const GOLD = '#C8A434' const cardBase: React.CSSProperties = { backgroundColor: 'rgba(10, 10, 12, 0.92)', border: '1px solid rgba(200, 164, 52, 0.08)', borderRadius: '12px', boxShadow: '0 0 20px rgba(200, 164, 52, 0.03)', } function stateColor(state: string): string { if (state === 'running') return '#2ECC71' if (state === 'paused') return '#E0A82E' if (state === 'exited' || state === 'dead') return '#E74C3C' return TEXT_SECONDARY } function formatBytes(bytes: number): string { if (bytes < 1024) return `${bytes} B` const units = ['KB', 'MB', 'GB', 'TB'] let v = bytes let i = -1 do { v /= 1024 i++ } while (v >= 1024 && i < units.length - 1) return `${v.toFixed(1)} ${units[i]}` } export default function Containers() { const [hosts, setHosts] = useState([]) const [integrationId, setIntegrationId] = useState('') const [containers, setContainers] = useState([]) const [error, setError] = useState(null) const [loading, setLoading] = useState(false) const [busyId, setBusyId] = useState(null) const [statsById, setStatsById] = useState>({}) const [logsContainer, setLogsContainer] = useState(null) const [execContainer, setExecContainer] = useState(null) useEffect(() => { api.listIntegrations().then(({ integrations }) => { const dockerHosts = integrations.filter((i) => i.type === 'docker') setHosts(dockerHosts) if (dockerHosts.length > 0) setIntegrationId(dockerHosts[0].id) }) }, []) function refresh() { if (!integrationId) return setLoading(true) setError(null) api .listContainers(integrationId) .then(({ containers }) => { setContainers(containers) containers.forEach((c) => { if (c.state !== 'running') return api .containerStats(integrationId, c.id) .then((stats) => setStatsById((prev) => ({ ...prev, [c.id]: stats }))) .catch(() => {}) }) }) .catch((err) => setError(err instanceof Error ? err.message : 'Failed to list containers')) .finally(() => setLoading(false)) } useEffect(() => { refresh() // eslint-disable-next-line react-hooks/exhaustive-deps }, [integrationId]) async function runAction(c: Container, action: 'start' | 'stop' | 'restart' | 'pause' | 'unpause') { if (!integrationId) return setBusyId(c.id) setError(null) try { await api.containerAction(integrationId, c.id, action) refresh() } catch (err) { setError(err instanceof Error ? err.message : `Failed to ${action} container`) } finally { setBusyId(null) } } async function removeContainer(c: Container) { if (!integrationId) return if (!confirm(`Remove container "${c.name}"? This cannot be undone.`)) return setBusyId(c.id) setError(null) try { await api.removeContainer(integrationId, c.id, c.state === 'running') refresh() } catch (err) { setError(err instanceof Error ? err.message : 'Failed to remove container') } finally { setBusyId(null) } } return (

Containers

Manage Docker containers across your configured hosts.

{error && (
{error}
)}
{containers.length === 0 && ( )} {containers.map((c) => { const stats = statsById[c.id] const busy = busyId === c.id return ( ) })}
Name Image State CPU Memory Ports Actions
{integrationId ? 'No containers found.' : 'Select a Docker integration to view containers.'}
{c.name} {c.image} {c.status} {stats ? `${stats.cpuPercent.toFixed(1)}%` : '—'} {stats ? `${formatBytes(stats.memUsage)} / ${formatBytes(stats.memLimit)}` : '—'} {c.ports.length === 0 ? '—' : c.ports.map((p) => `${p.publicPort ?? ''}${p.publicPort ? ':' : ''}${p.privatePort}/${p.type}`).join(', ')}
{c.state === 'running' ? ( <> ) : c.state === 'paused' ? ( ) : ( )}
{logsContainer && integrationId && ( setLogsContainer(null)} /> )} {execContainer && integrationId && ( setExecContainer(null)} /> )}
) } function LogsModal({ integrationId, container, onClose }: { integrationId: number; container: Container; onClose: () => void }) { const [logs, setLogs] = useState('') const [loading, setLoading] = useState(true) const [error, setError] = useState(null) function load() { setLoading(true) setError(null) api .containerLogs(integrationId, container.id) .then(({ logs }) => setLogs(logs)) .catch((err) => setError(err instanceof Error ? err.message : 'Failed to fetch logs')) .finally(() => setLoading(false)) } useEffect(load, []) return (

Logs — {container.name}

{error &&

{error}

}
          {logs || (loading ? 'Loading…' : 'No logs.')}
        
) } function ExecModal({ integrationId, container, onClose }: { integrationId: number; container: Container; onClose: () => void }) { const containerRef = useRef(null) const wsRef = useRef(null) const [connected, setConnected] = useState(false) useEffect(() => { if (!containerRef.current) return const term = new XTerm({ cursorBlink: true, fontSize: 13, fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', theme: { background: '#15161A', foreground: '#E8E6E0', cursor: GOLD }, }) const fit = new FitAddon() term.loadAddon(fit) term.open(containerRef.current) fit.fit() const token = getToken() const proto = window.location.protocol === 'https:' ? 'wss' : 'ws' const ws = new WebSocket(`${proto}://${window.location.host}/api/docker/exec?token=${encodeURIComponent(token ?? '')}`) wsRef.current = ws ws.onopen = () => { ws.send(JSON.stringify({ type: 'connect', integrationId, containerId: container.id, cols: term.cols, rows: term.rows })) } ws.onmessage = (event) => { const msg = JSON.parse(event.data) if (msg.type === 'ready') { setConnected(true) } else if (msg.type === 'data') { term.write(Uint8Array.from(atob(msg.data), (c) => c.charCodeAt(0))) } else if (msg.type === 'error') { term.writeln(`\r\n\x1b[31mError: ${msg.message}\x1b[0m`) setConnected(false) } else if (msg.type === 'exit') { term.writeln('\r\n\x1b[33mSession ended.\x1b[0m') setConnected(false) } } ws.onclose = () => setConnected(false) term.onData((data) => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'input', data: btoa(data) })) } }) term.onResize(({ cols, rows }) => { if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'resize', cols, rows })) }) const onResize = () => fit.fit() window.addEventListener('resize', onResize) return () => { window.removeEventListener('resize', onResize) if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'disconnect' })) ws.close() term.dispose() } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) return (

Exec — {container.name}

) }