import { useCallback, useEffect, useRef, useState } from 'react'; import { CircleDot, Hammer, Loader2, Play, Square } from 'lucide-react'; import { useClient } from 'hooks/useClient'; import { Button } from '@/components/ui/button'; import { readableError } from './errors'; import { Console } from './Console'; /** * Production: the built site, and the nginx serving it. * * ── Two states, not one ── * * A build can succeed while nothing serves it, and a container can serve a directory built weeks ago. * They fail independently and they are fixed differently, so they are reported separately rather than * collapsed into "production is up" — which would be true in one of those cases and misleading in the * other. * * ── Build takes minutes ── * * It renders every route through the real app and copies every referenced file. Said in the button, * because a spinner sitting for three minutes with no explanation is indistinguishable from a hang. */ type Static = { path: string; built: boolean; entries: number | null; builtAt: string | null; container: { name: string; state: string | null; forward: string }; }; const when = (iso: string) => new Date(iso).toLocaleString(undefined, { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }); export const StaticBlock = () => { const client = useClient(); const clientRef = useRef(client); clientRef.current = client; const [status, setStatus] = useState(null); const [busy, setBusy] = useState<'build' | 'start' | 'stop' | null>(null); const [output, setOutput] = useState(null); const [failed, setFailed] = useState(false); const load = useCallback(async () => { try { setStatus(await clientRef.current.get('/fofos-admin/_config/static')); } catch { setStatus(null); } }, []); useEffect(() => { void load(); }, [load]); const act = async (what: 'build' | 'start' | 'stop') => { setBusy(what); setFailed(false); setOutput(what === 'build' ? 'Rendering every page. This takes a few minutes.' : null); try { const path = what === 'build' ? '/fofos-admin/_config/static/build' : `/fofos-admin/_config/static/${what}`; const result = await clientRef.current.post<{ ok: boolean; output: string; status: Static }>(path, {}); setStatus(result.status); setOutput(result.output); setFailed(!result.ok); } catch (failure) { setOutput(readableError(failure)); setFailed(true); void load(); } finally { setBusy(null); } }; if (!status) return null; const serving = status.container.state === 'running'; return (

Production {status.built ? `${status.entries} files` : 'not built'} {status.builtAt ? ` · built ${when(status.builtAt)}` : ''} {' · '} {status.container.state ?? 'no server'}

{/* The forward target, because it is what a domain has to point at and it is not a port. */}

{status.container.name} · {serving ? status.container.forward : 'static files, no port'}

); };