the site gets what it needs to boot, and the status stops lying

Three things, and the second is why the panel was confidently wrong.

The site refuses to start without FOFOS_API_TOKEN, ADMIN_SESSION_SECRET,
PUBLIC_URL, API_URL and FOFOS_MEDIA_DIR, and the checkout has no .env at all.
Every one of those is either already known here — the port, the media directory
— or a random secret nobody should be asked to invent. So they are generated
once, kept encrypted under this plugin's own purpose key, and supplied. Once,
not per start: regenerating would invalidate every session and token anybody had
noted down.

pm2 saying `online` is NOT the site being up. `bun --hot` survives a crash at
startup — the process stays alive, pm2 reports online, and nothing is listening.
That is precisely what development did: reported up, served nothing, and this
panel repeated it. The status now also asks whether the port answers, any
response counting, and says "not answering" out loud when the two disagree.

And the buttons are a fixed width, because Start and Restart are different
lengths and rows that size to their own label leave the inputs on different
vertical lines.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-29 13:50:16 +01:00
co-authored by Claude Opus 5
parent 2e56260a88
commit ca43f7ef86
3 changed files with 128 additions and 15 deletions
+15 -2
View File
@@ -37,6 +37,7 @@ type Server = {
uptimeMs: number | null;
restarts: number | null;
url: string | null;
reachable: boolean | null;
};
const uptime = (ms: number) => {
@@ -120,7 +121,10 @@ export const ServersPanel = () => {
prod !== String(servers?.find((s) => s.name === 'production')?.port ?? '');
const row = (server: Server, value: string, onChange: (next: string) => void) => {
// pm2 saying `online` is not the same as the site answering — `bun --hot` stays alive after a
// startup crash and serves nothing. The dot follows what ANSWERS; the words report both.
const online = server.status === 'online';
const serving = server.reachable === true;
const label = server.name === 'development' ? 'Development' : 'Production';
return (
@@ -129,17 +133,22 @@ export const ServersPanel = () => {
<p className="flex items-center gap-1.5 text-xs">
{/* pm2's own word, not an interpretation of it. `errored` and `stopped` mean different
things to whoever has to fix it. */}
<CircleDot className={`size-3 ${online ? 'text-success' : server.status ? 'text-destructive' : 'text-muted-foreground/40'}`} />
<CircleDot
className={`size-3 ${serving ? 'text-success' : server.status ? 'text-destructive' : 'text-muted-foreground/40'}`}
/>
{label}
<span className="text-muted-foreground/70">
{server.status ?? 'not started'}
{/* Said out loud, because it is the case that misleads: the process is up and the site is
not there. */}
{online && !serving ? ' · not answering' : ''}
{online && server.uptimeMs !== null ? ` · up ${uptime(server.uptimeMs)}` : ''}
{server.restarts ? ` · ${server.restarts} restarts` : ''}
</span>
</p>
<p className="font-mono text-xs text-muted-foreground/60">
{server.process}
{online && server.url ? ` · ${server.url}` : ''}
{serving && server.url ? ` · ${server.url}` : ''}
</p>
</div>
@@ -152,10 +161,13 @@ export const ServersPanel = () => {
autoComplete="off"
/>
{/* Fixed widths, because "Start" and "Restart" are different lengths and a row that sizes to its
own label leaves the two rows' inputs and buttons on different vertical lines. */}
<div className="flex shrink-0 gap-2">
<Button
variant="outline"
size="sm"
className="w-24 justify-center"
onClick={() => void act(server.name, 'start')}
// Dirty ports are not in effect until saved, and starting with the old one silently would
// be the confusing outcome.
@@ -168,6 +180,7 @@ export const ServersPanel = () => {
<Button
variant="outline"
size="sm"
className="w-24 justify-center"
onClick={() => void act(server.name, 'stop')}
disabled={busy !== null || !server.status}
>