diff --git a/api/connection.ts b/api/connection.ts index 6446315..4bd9ef7 100644 --- a/api/connection.ts +++ b/api/connection.ts @@ -1,7 +1,7 @@ import { db } from 'officerdb/db'; import { encryptSecret, decryptSecret } from 'officerdb/crypto'; import { eq } from 'drizzle-orm'; -import { fofosAdminConnection, type FofosAdminConnection } from '../db/schema'; +import { fofosAdminConnection, fofosAdminSettings, type FofosAdminConnection } from '../db/schema'; import { normaliseUrl, type FofosCredentials } from './fofos-client'; /** @@ -56,10 +56,33 @@ export async function readConnection(): Promise { } /** Decrypted, for the client only. Never serialise the result of this. */ +/** + * How to reach the site's admin API — DERIVED now, not stored. + * + * ── Why there is nothing to configure ── + * + * This used to read a URL and a token somebody typed into a form. Both are now facts this plugin + * already holds: the development server runs on a port it chose, and `FOFOS_API_TOKEN` is a secret it + * generated and handed to that server at start. Asking a person to supply either would be asking them + * to copy a value out of one part of this plugin and into another. + * + * ── Why DEVELOPMENT and not production ── + * + * Production is a directory of files behind nginx. It has no API, no database connection and nothing + * listening — editing against it is not merely wrong, it is impossible. So the sections read and write + * the development server, and Build is what publishes. That is the shape the site's own build script + * already implies, and it means an edit is visible on dev immediately and public only when somebody + * decides it should be. + * + * Null when there is no port or no secret yet, which the panel reports as not being set up. + */ export async function readCredentials(): Promise { - const found = await row(); - if (!found) return null; - return { url: found.url, token: decryptSecret(PURPOSE, found.token) }; + const settings = (await db.select().from(fofosAdminSettings).limit(1))[0] ?? null; + if (!settings?.devPort || !settings.siteApiToken) return null; + return { + url: `http://127.0.0.1:${settings.devPort}`, + token: decryptSecret('fofos-admin', settings.siteApiToken), + }; } export type SaveParams = { url: string; token?: string }; diff --git a/api/router.ts b/api/router.ts index f481fbb..fa94fe2 100644 --- a/api/router.ts +++ b/api/router.ts @@ -539,10 +539,26 @@ router.get('/status', async (ctx) => { that answer. The path itself stays behind `/_config`, which is owner-only — this says only whether one has been chosen, which reveals nothing about the machine. */ + /* + `ready` is whether the site sections can be OPENED, and it is now answerable. + + It was hardcoded false for as long as there was nothing behind them: the sections proxy to the + site's admin API, and no address for one was ever stored. Both halves exist now — the development + server runs on a port this plugin chose, holding a token this plugin generated — so the question + became a real one and this is the answer. + + It asks whether the server is ANSWERING, 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. + */ + const servers = await readServers(); + const development = servers.find((server) => server.name === 'development'); + return ctx.json({ configured: connection.configured, lastProbeOk: connection.lastProbeOk, rootSet: settings.isSet, + ready: development?.reachable === true, }); }); diff --git a/web/useReady.ts b/web/useReady.ts index 7ff5fed..050a0fa 100644 --- a/web/useReady.ts +++ b/web/useReady.ts @@ -19,11 +19,11 @@ import { useFofosChanged } from './channel'; * 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. * - * ── Hardcoded false, deliberately, for now ── + ── It is answered by the server ── * - * The shape is real and the plumbing is real: it asks `/status`, it re-asks whenever the connection - * changes, and it reports while it is still asking. Only the final verdict is pinned, so turning the - * gate on is one line rather than a feature. + * 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 ── * @@ -33,13 +33,13 @@ import { useFofosChanged } from './channel'; * here rather than hidden in a wrapper. If the condition grows expensive, that is the moment to fix * it, not before. * - * What it will become is roughly `configured && lastProbeOk` — the connection is stored AND the last - * probe reached fofos — but "all the info needed in Administration" is not settled yet and will grow - * past those two. That is why this returns a single boolean and not the parts: callers must not start - * assembling their own version of the condition out of the pieces. + * 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 }; +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. */ @@ -73,7 +73,15 @@ export const useReady = (): Readiness => { "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. */ - setStatus({ configured: true, lastProbeOk: null, rootSet: false }); + /* + `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 }); } }, []); @@ -81,10 +89,18 @@ export const useReady = (): Readiness => { void check(); }, [check, signal]); - // ──────────────────────────────────────────────────────────────────────────────────────────────── - // The gate. Replace `false` with the real condition — nothing else here has to change. - // ──────────────────────────────────────────────────────────────────────────────────────────────── - const ready = false; + /* + 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 }; };