Files
fofos-admin/web/useReady.ts
T
pastilhasandClaude Opus 5 f0d29c8000 the site sections open, because there is finally something behind them
`ready` was hardcoded false for as long as the sections proxied to an address
nobody could set — the connection form was removed and nothing replaced it, so
nine doors led to the same "not connected" message.

Both halves exist now and neither is typed by a person: the development server
runs on a port this plugin chose, holding a FOFOS_API_TOKEN this plugin
generated and handed it at start. So the credentials are DERIVED rather than
stored. Asking somebody to copy a value out of one part of this plugin into
another was never a setting, it was a chore.

Development and not production, and that is not a preference. Production is a
directory of files behind nginx — no API, no database, nothing listening — so
editing against it is impossible rather than merely wrong. Sections read and
write dev; Build is what publishes. An edit is visible immediately and public
only when somebody decides it should be.

The gate asks whether the server ANSWERS, not whether pm2 says online. `bun
--hot` survives a crash at startup: the process stays up, pm2 reports online,
and nothing is listening. Opening nine sections onto that would make one failed
boot look like nine broken screens.

And when /status itself cannot be reached, `configured` stays true while `ready`
goes false. Telling somebody they have set nothing up is alarming and wrong;
opening nine sections onto a server we could not confirm is answering is nine
screens that each fail separately. Cannot-tell keeps them shut and lets the next
poll open them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 20:00:18 +01:00

107 lines
5.0 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from 'react';
import { useClient } from 'hooks/useClient';
import { useFofosChanged } from './channel';
/**
* Is this plugin ready to be USED — that is, may anything but Administration be opened?
*
* ── One condition, read in two places ──
*
* The nav decides whether a section is clickable and the detail panel decides what to render. Those
* are two answers to the same question, and a nav that lets you click a row the panel then refuses is
* worse than one that does not offer it. So the condition lives here and both panels read it, rather
* than each deciding for itself — the panels are siblings and neither may own the other.
*
* ── It is a GATE, not a hint ──
*
* Not ready means the site sections cannot be reached at all, by clicking or by typing `?section=` in
* the address bar. Every one of them is a window onto fofos, and without a working connection there is
* nothing behind any of them; offering nine doors onto the same "not connected" message taught nobody
* anything and made a setup step look like nine faults.
*
── It is answered by the server ──
*
* This was pinned to `false` for as long as nothing was behind the sections. What changed is not this
* hook: the site's admin API is served by the development server, which this plugin now starts on a
* port it chose with a token it generated. So there is something to ask about, and `/status` asks it.
*
* ── Both panels call this, so `/status` is fetched twice ──
*
* Known and accepted for now: two cheap GETs that answer the same question, kept in step because both
* re-run on the same `useFofosChanged` signal. The alternative is a context provider around the two
* panels, which is a shared-state boundary this does not yet need — and the duplication is visible
* here rather than hidden in a wrapper. If the condition grows expensive, that is the moment to fix
* it, not before.
*
* It returns a single boolean and not the parts, and that still matters: callers must not start
* assembling their own version of the condition, because the condition has already changed shape once
* — from "a stored HTTP connection" to "the development server is answering" — and every caller that
* had built its own would have been quietly wrong on the day it did.
*/
type Status = { configured: boolean; lastProbeOk: boolean | null; rootSet: boolean; ready: boolean };
export type Readiness = {
/** The fofos connection works. Gates every section that READS the site. */
ready: boolean;
/**
* A root location has been chosen.
*
* Separate from `ready` because the two are genuinely different questions and one section depends on
* each. Media needs a folder on this machine; it never speaks to fofos, and gating it behind a
* connection it does not use was the bug that made this distinction necessary.
*/
rootSet: boolean;
/** Still asking. Callers should render nothing rather than guess — see below. */
loading: boolean;
};
export const useReady = (): Readiness => {
const client = useClient();
const clientRef = useRef(client);
clientRef.current = client;
const [status, setStatus] = useState<Status | null>(null);
const [signal] = useFofosChanged();
const check = useCallback(async () => {
try {
setStatus(await clientRef.current.get<Status>('/fofos-admin/status'));
} catch {
/*
Cannot tell. Treated as CONFIGURED rather than not — a transient failure must never present as
"you have not set this up", which is a far more alarming thing to be told and invites somebody
to re-enter a connection that was never the problem.
*/
/*
`ready: false` here, unlike `configured`, and the asymmetry is deliberate.
Being told "you have not set this up" when the request merely failed is alarming and wrong, so
configured stays true. But OPENING nine sections onto a server we could not confirm is
answering would show nine screens that each fail on their own — the honest read of "cannot
tell" is to keep them shut and let the next poll open them.
*/
setStatus({ configured: true, lastProbeOk: null, rootSet: false, ready: false });
}
}, []);
useEffect(() => {
void check();
}, [check, signal]);
/*
The gate, and it is the server's answer rather than one assembled here.
It was `false` for as long as there was nothing behind the sections. The server now knows: the
development server is what serves the site's admin API, so `ready` is whether that is ANSWERING —
not whether pm2 says online, because `bun --hot` stays alive after a startup crash and would have
made one failed boot look like nine broken screens.
Computed there and not here on purpose. The note above says callers must not assemble their own
version out of the parts, and that applies to this hook too: one place decides, everything reads it.
*/
const ready = status?.ready ?? false;
return { ready, rootSet: status?.rootSet ?? false, loading: status === null };
};