import { useCallback, useEffect, useRef, useState } from 'react'; import { Check, CircleDot, Loader2, Play, Server, Square } from 'lucide-react'; import { useClient } from 'hooks/useClient'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { readableError } from './errors'; import { Block } from './Block'; import { Console } from './Console'; import { useFlash } from './useFlash'; /** * The two servers that serve the site, under pm2. * * ── Both under pm2, development included ── * * A `bun dev` started in a terminal dies with the terminal, and nobody can tell whether it is running * without finding the window it was started in. Under pm2 the two are the same kind of thing: a named * process with a status and an uptime, startable from here and inspectable from anywhere. * * ── Start is really a replace ── * * The port and the database come from these settings, and pm2 keeps the environment a process was * STARTED with. So starting removes any existing process of that name first — otherwise a port changed * here would be saved, shown, and not in effect, which is the kind of thing somebody debugs for an hour. * * ── The process names are shown ── * * `fofos-dev` and `fofos-prod`, in the row. Whatever this panel can do, `pm2 logs fofos-dev` does more, * and somebody who knows that should not have to guess the name to use it. */ type Server = { name: 'development' | 'production'; process: string; port: number | null; status: string | null; uptimeMs: number | null; restarts: number | null; url: string | null; }; const uptime = (ms: number) => { const minutes = Math.floor(ms / 60000); if (minutes < 60) return `${minutes}m`; const hours = Math.floor(minutes / 60); return hours < 24 ? `${hours}h ${minutes % 60}m` : `${Math.floor(hours / 24)}d ${hours % 24}h`; }; export const ServersPanel = () => { const client = useClient(); const clientRef = useRef(client); clientRef.current = client; const [servers, setServers] = useState(null); const [dev, setDev] = useState(''); const [prod, setProd] = useState(''); const [busy, setBusy] = useState(null); const [error, setError] = useState(null); const [output, setOutput] = useState(null); const [failed, setFailed] = useState(false); const [flashing, flash] = useFlash(); const load = useCallback(async () => { try { const current = await clientRef.current.get('/fofos-admin/_config/servers'); setServers(current); setDev((typed) => typed || String(current.find((s) => s.name === 'development')?.port ?? '')); setProd((typed) => typed || String(current.find((s) => s.name === 'production')?.port ?? '')); } catch { setServers(null); } }, []); useEffect(() => { void load(); }, [load]); const savePorts = async () => { setBusy('ports'); setError(null); try { setServers( await clientRef.current.put('/fofos-admin/_config/servers', { development: Number(dev), production: Number(prod), }), ); flash(); } catch (failure) { setError(readableError(failure)); } finally { setBusy(null); } }; const act = async (which: string, action: 'start' | 'stop') => { setBusy(`${which}:${action}`); setError(null); setFailed(false); setOutput(null); try { const result = await clientRef.current.post<{ ok: boolean; output: string; servers: Server[] }>( `/fofos-admin/_config/servers/${which}/${action}`, {}, ); setServers(result.servers); setOutput(result.output); setFailed(!result.ok); } catch (failure) { setOutput(readableError(failure)); setFailed(true); void load(); } finally { setBusy(null); } }; const dirty = dev !== String(servers?.find((s) => s.name === 'development')?.port ?? '') || prod !== String(servers?.find((s) => s.name === 'production')?.port ?? ''); const row = (server: Server, value: string, onChange: (next: string) => void) => { const online = server.status === 'online'; const label = server.name === 'development' ? 'Development' : 'Production'; return (

{/* pm2's own word, not an interpretation of it. `errored` and `stopped` mean different things to whoever has to fix it. */} {label} {server.status ?? 'not started'} {online && server.uptimeMs !== null ? ` · up ${uptime(server.uptimeMs)}` : ''} {server.restarts ? ` · ${server.restarts} restarts` : ''}

{server.process} {online && server.url ? ` · ${server.url}` : ''}

onChange(event.target.value.replace(/[^0-9]/g, ''))} placeholder="3010" className="w-24 shrink-0 font-mono text-xs" inputMode="numeric" autoComplete="off" />
); }; return (
Saved ) : undefined } > {!servers ? null : ( <> {servers.map((server) => server.name === 'development' ? row(server, dev, setDev) : row(server, prod, setProd), )} {error &&

{error}

} {dirty && (
)} )}
); };