'use client' import { useCallback, useEffect, useRef, useState } from 'react' const API_BASE = '/api/v1' type ContainerStatus = 'PENDING' | 'CREATING' | 'RUNNING' | 'STOPPED' | 'RESTARTING' | 'REMOVING' | 'ERROR' interface CompanyContainer { id: string companyId: string dockerId: string | null containerName: string status: ContainerStatus port: number image: string errorMessage: string | null createdAt: string updatedAt: string company: { id: string name: string slug: string status: string } } function authHeaders() { const token = typeof window !== 'undefined' ? localStorage.getItem('admin_token') : '' return { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` } } function StatusBadge({ status }: { status: ContainerStatus }) { const map: Record = { PENDING: { label: 'Pending', className: 'bg-zinc-700 text-zinc-300' }, CREATING: { label: 'Creating…', className: 'bg-blue-900 text-blue-300 animate-pulse' }, RUNNING: { label: 'Running', className: 'bg-emerald-900 text-emerald-300' }, STOPPED: { label: 'Stopped', className: 'bg-yellow-900 text-yellow-300' }, RESTARTING: { label: 'Restarting…',className: 'bg-orange-900 text-orange-300 animate-pulse' }, REMOVING: { label: 'Removing…', className: 'bg-red-900 text-red-300 animate-pulse' }, ERROR: { label: 'Error', className: 'bg-red-950 text-red-400' }, } const { label, className } = map[status] ?? map.ERROR return ( {label} ) } function LogsModal({ companyId, companyName, onClose }: { companyId: string; companyName: string; onClose: () => void }) { const [logs, setLogs] = useState('') const [loading, setLoading] = useState(true) const [tail, setTail] = useState(150) const bottomRef = useRef(null) const fetchLogs = useCallback(async (lines: number) => { setLoading(true) try { const res = await fetch(`${API_BASE}/admin/containers/${companyId}/logs?tail=${lines}`, { headers: authHeaders() }) const json = await res.json() setLogs(json.data?.logs ?? '') } catch { setLogs('Failed to fetch logs.') } finally { setLoading(false) } }, [companyId]) useEffect(() => { fetchLogs(tail) }, [fetchLogs, tail]) useEffect(() => { bottomRef.current?.scrollIntoView() }, [logs]) return (
e.stopPropagation()}>

Container Logs

{companyName}

{loading ? (

Loading…

) : (
              {logs || 'No logs available.'}
            
)}
) } type ProvisionResult = { companyId: string; name: string; status: 'created' | 'error'; error?: string } export default function ContainersPage() { const [containers, setContainers] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [busy, setBusy] = useState>({}) const [logsFor, setLogsFor] = useState<{ companyId: string; companyName: string } | null>(null) const [search, setSearch] = useState('') const [provisioning, setProvisioning] = useState(false) const [provisionResults, setProvisionResults] = useState(null) const fetchContainers = useCallback(async () => { try { const res = await fetch(`${API_BASE}/admin/containers`, { headers: authHeaders() }) const json = await res.json().catch(() => null) if (!res.ok) { throw new Error(json?.message ?? 'Failed to load containers.') } setContainers(json.data ?? []) setError(null) } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load containers.') /* silent — keep old data */ } finally { setLoading(false) } }, []) useEffect(() => { fetchContainers() const id = setInterval(fetchContainers, 8000) return () => clearInterval(id) }, [fetchContainers]) async function provisionAll() { setProvisioning(true) setProvisionResults(null) try { const res = await fetch(`${API_BASE}/admin/containers/provision-all`, { method: 'POST', headers: authHeaders() }) const json = await res.json().catch(() => null) if (!res.ok) throw new Error(json?.message ?? 'Provisioning failed.') setProvisionResults(json.data?.results ?? []) setError(null) await fetchContainers() } catch (err) { setError(err instanceof Error ? err.message : 'Provisioning failed.') } finally { setProvisioning(false) } } async function act(companyId: string, action: 'start' | 'stop' | 'restart' | 'deploy' | 'remove') { setBusy((b) => ({ ...b, [companyId]: true })) try { const method = action === 'remove' ? 'DELETE' : 'POST' const url = action === 'remove' ? `${API_BASE}/admin/containers/${companyId}` : `${API_BASE}/admin/containers/${companyId}/${action}` const res = await fetch(url, { method, headers: authHeaders() }) const json = await res.json().catch(() => null) if (!res.ok) { throw new Error(json?.message ?? `Failed to ${action} container.`) } setError(null) await fetchContainers() } catch (err) { setError(err instanceof Error ? err.message : `Failed to ${action} container.`) } finally { setBusy((b) => ({ ...b, [companyId]: false })) } } const filtered = containers.filter( (c) => c.company.name.toLowerCase().includes(search.toLowerCase()) || c.containerName.toLowerCase().includes(search.toLowerCase()) || c.company.slug.toLowerCase().includes(search.toLowerCase()), ) const stats = { running: containers.filter((c) => c.status === 'RUNNING').length, stopped: containers.filter((c) => c.status === 'STOPPED').length, error: containers.filter((c) => c.status === 'ERROR').length, total: containers.length, } return (
{logsFor && ( setLogsFor(null)} /> )}

Containers

Manage isolated Docker Compose services for each company workspace.

{error && (
{error}
)} {provisionResults !== null && (

Provisioning complete —{' '} {provisionResults.filter((r) => r.status === 'created').length} created {provisionResults.some((r) => r.status === 'error') && ( <>, {provisionResults.filter((r) => r.status === 'error').length} failed )}

{provisionResults.map((r) => (
{r.name} {r.status === 'error' && {r.error}} {r.status === 'created' && Service created}
))}
)} {/* Stats */}
{[ { label: 'Total', value: stats.total, color: 'text-zinc-100' }, { label: 'Running', value: stats.running, color: 'text-emerald-400' }, { label: 'Stopped', value: stats.stopped, color: 'text-yellow-400' }, { label: 'Error', value: stats.error, color: 'text-red-400' }, ].map((s) => (

{s.label}

{s.value}

))}
{/* Search */}
setSearch(e.target.value)} className="w-full max-w-sm rounded-xl border border-zinc-700 bg-zinc-800 px-4 py-2 text-sm text-zinc-200 placeholder-zinc-500 focus:outline-none focus:ring-1 focus:ring-emerald-500" />
{/* Table */}
{loading ? (
) : filtered.length === 0 ? (
{search ? 'No containers match your search.' : 'No containers yet. They are created automatically on company signup.'}
) : ( {filtered.map((c) => { const isBusy = busy[c.companyId] ?? false const isRunning = c.status === 'RUNNING' const isStopped = c.status === 'STOPPED' || c.status === 'ERROR' const isTransitioning = ['CREATING', 'RESTARTING', 'REMOVING'].includes(c.status) return ( ) })}
Company Container Status Port Image Actions

{c.company.name}

{c.company.slug}

{c.errorMessage && (

{c.errorMessage.slice(0, 60)}{c.errorMessage.length > 60 ? '…' : ''}

)}
{c.containerName} {c.dockerId && (

{c.dockerId.slice(0, 12)}

)}
:{c.port} {c.image}
{isStopped && ( act(c.companyId, 'start')} /> )} {isRunning && ( act(c.companyId, 'stop')} /> )} {(isRunning || isStopped) && ( act(c.companyId, 'restart')} /> )} act(c.companyId, 'deploy')} /> setLogsFor({ companyId: c.companyId, companyName: c.company.name })} /> { if (confirm(`Remove container for ${c.company.name}? This cannot be undone.`)) { act(c.companyId, 'remove') } }} />
)}
) } function ActionButton({ label, color, disabled, onClick, }: { label: string color: 'emerald' | 'yellow' | 'blue' | 'purple' | 'zinc' | 'red' disabled: boolean onClick: () => void }) { const colorMap: Record = { emerald: 'border-emerald-800 text-emerald-400 hover:bg-emerald-900/40', yellow: 'border-yellow-800 text-yellow-400 hover:bg-yellow-900/40', blue: 'border-blue-800 text-blue-400 hover:bg-blue-900/40', purple: 'border-purple-800 text-purple-400 hover:bg-purple-900/40', zinc: 'border-zinc-700 text-zinc-400 hover:bg-zinc-700/40', red: 'border-red-900 text-red-400 hover:bg-red-900/30', } return ( ) }