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>
This commit is contained in:
+27
-4
@@ -1,7 +1,7 @@
|
|||||||
import { db } from 'officerdb/db';
|
import { db } from 'officerdb/db';
|
||||||
import { encryptSecret, decryptSecret } from 'officerdb/crypto';
|
import { encryptSecret, decryptSecret } from 'officerdb/crypto';
|
||||||
import { eq } from 'drizzle-orm';
|
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';
|
import { normaliseUrl, type FofosCredentials } from './fofos-client';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,10 +56,33 @@ export async function readConnection(): Promise<ConnectionView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Decrypted, for the client only. Never serialise the result of this. */
|
/** 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<FofosCredentials | null> {
|
export async function readCredentials(): Promise<FofosCredentials | null> {
|
||||||
const found = await row();
|
const settings = (await db.select().from(fofosAdminSettings).limit(1))[0] ?? null;
|
||||||
if (!found) return null;
|
if (!settings?.devPort || !settings.siteApiToken) return null;
|
||||||
return { url: found.url, token: decryptSecret(PURPOSE, found.token) };
|
return {
|
||||||
|
url: `http://127.0.0.1:${settings.devPort}`,
|
||||||
|
token: decryptSecret('fofos-admin', settings.siteApiToken),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SaveParams = { url: string; token?: string };
|
export type SaveParams = { url: string; token?: string };
|
||||||
|
|||||||
@@ -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
|
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.
|
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({
|
return ctx.json({
|
||||||
configured: connection.configured,
|
configured: connection.configured,
|
||||||
lastProbeOk: connection.lastProbeOk,
|
lastProbeOk: connection.lastProbeOk,
|
||||||
rootSet: settings.isSet,
|
rootSet: settings.isSet,
|
||||||
|
ready: development?.reachable === true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+30
-14
@@ -19,11 +19,11 @@ import { useFofosChanged } from './channel';
|
|||||||
* nothing behind any of them; offering nine doors onto the same "not connected" message taught nobody
|
* 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.
|
* 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
|
* This was pinned to `false` for as long as nothing was behind the sections. What changed is not this
|
||||||
* changes, and it reports while it is still asking. Only the final verdict is pinned, so turning the
|
* hook: the site's admin API is served by the development server, which this plugin now starts on a
|
||||||
* gate on is one line rather than a feature.
|
* 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 ──
|
* ── 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
|
* here rather than hidden in a wrapper. If the condition grows expensive, that is the moment to fix
|
||||||
* it, not before.
|
* it, not before.
|
||||||
*
|
*
|
||||||
* What it will become is roughly `configured && lastProbeOk` — the connection is stored AND the last
|
* It returns a single boolean and not the parts, and that still matters: callers must not start
|
||||||
* probe reached fofos — but "all the info needed in Administration" is not settled yet and will grow
|
* assembling their own version of the condition, because the condition has already changed shape once
|
||||||
* past those two. That is why this returns a single boolean and not the parts: callers must not start
|
* — from "a stored HTTP connection" to "the development server is answering" — and every caller that
|
||||||
* assembling their own version of the condition out of the pieces.
|
* 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 = {
|
export type Readiness = {
|
||||||
/** The fofos connection works. Gates every section that READS the site. */
|
/** 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
|
"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.
|
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();
|
void check();
|
||||||
}, [check, signal]);
|
}, [check, signal]);
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────────────────────────
|
/*
|
||||||
// The gate. Replace `false` with the real condition — nothing else here has to change.
|
The gate, and it is the server's answer rather than one assembled here.
|
||||||
// ────────────────────────────────────────────────────────────────────────────────────────────────
|
|
||||||
const ready = false;
|
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 };
|
return { ready, rootSet: status?.rootSet ?? false, loading: status === null };
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user