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(null); const [signal] = useFofosChanged(); const check = useCallback(async () => { try { setStatus(await clientRef.current.get('/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 }; };