From 10b7bc34f7e5cb1e9abecbdd63eae16b4ede8dc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sat, 29 Aug 2026 00:25:57 +0100 Subject: [PATCH] nothing but Administration opens until the plugin is ready MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit every section was clickable and every one of them showed the same connection form, because the panel forced it and the nav did not know. nine doors onto one message taught nobody anything and made a setup step look like nine faults. so there is now a gate, `useReady`, and both panels read it. that is the point of putting it in a hook rather than in either panel: a nav that offers a row the detail panel then refuses is worse than one that does not offer it. the nav DECLINES TO OFFER — a locked section is a
, not a , so there is no href to hover, no cmd-click into a new tab and nothing to focus. rendering a link and swallowing the click leaves a row that looks navigable and behaves like one everywhere except the click. the detail panel HOLDS THE LOCK, and it has to, because `?section=gallery` typed into the address bar or arriving from an old bookmark never passes through the nav. one line of copy in the foot rather than a badge on nine dimmed rows. without it a locked nav reads as broken instead of as one step not done: the rows go grey and nothing says why or what to do about it. THE CONDITION IS HARDCODED FALSE, as asked. the plumbing around it is real — it asks /status, re-asks when the connection changes, and reports while it is still asking — so switching the gate on is one line. it returns a single boolean and not its parts deliberately: "all the info needed in Administration" will grow past `configured && lastProbeOk`, and callers must not start assembling their own version of it out of the pieces. Co-Authored-By: Claude Opus 5 --- manifest.ts | 2 +- web/DetailPanel.tsx | 49 +++++++-------------------- web/NavPanel.tsx | 46 +++++++++++++++++++++---- web/useReady.ts | 82 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 134 insertions(+), 45 deletions(-) create mode 100644 web/useReady.ts diff --git a/manifest.ts b/manifest.ts index bb5cf8b..19020c7 100644 --- a/manifest.ts +++ b/manifest.ts @@ -35,7 +35,7 @@ import type { PluginManifest } from '@@/plugins/manifest'; */ export const manifest: PluginManifest = { publisher: 'officerdev', - version: '2026.08.29.03', + version: '2026.08.29.04', platform: '>=1.0.0', label: 'Fofos', diff --git a/web/DetailPanel.tsx b/web/DetailPanel.tsx index 23f4ac1..8d29f4f 100644 --- a/web/DetailPanel.tsx +++ b/web/DetailPanel.tsx @@ -1,11 +1,9 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; import { useSearchParams } from 'react-router'; -import { useClient } from 'hooks/useClient'; import { DEFAULT_SECTION, isSectionKey, sectionFor, type SectionKey } from './sections'; import { ConnectionPanel } from './ConnectionPanel'; import { MessagesPanel } from './MessagesPanel'; import { PlannedSection } from './PlannedSection'; -import { useFofosChanged } from './channel'; +import { useReady } from './useReady'; /** * The right column: whichever section the nav has selected. @@ -20,16 +18,14 @@ import { useFofosChanged } from './channel'; * to the same screen is a second thing to keep correct, and the two disagreed the moment a section was * selected underneath a form that covered it. * - * ── The unconfigured check lives HERE, once ── + * ── Not ready means Administration, whatever the URL says ── * - * Every OTHER section needs a connection, so each would otherwise have to handle its absence and each - * would word it slightly differently. Asking `/status` once and sending an unconfigured plugin to - * Administration is one answer for all of them, and it makes "not connected yet" read as a setup step - * rather than as nine broken lists. + * The gate is `useReady`, shared with the nav so the two cannot disagree. Enforced HERE and not only + * there, because the nav can only decline to offer a link — `?section=gallery` typed into the address + * bar, or a bookmark from before the connection was forgotten, arrives without going through it. A + * nav that hides a door is a courtesy; this is the lock. */ -type Status = { configured: boolean; lastProbeOk: boolean | null }; - /** * Which component renders which section. * @@ -50,39 +46,18 @@ export const DetailPanel = () => { const raw = params.get('section'); const selected: SectionKey = isSectionKey(raw) ? raw : DEFAULT_SECTION; - const client = useClient(); - const clientRef = useRef(client); - clientRef.current = client; + const { ready, loading } = useReady(); - const [configured, setConfigured] = useState(null); - const [signal] = useFofosChanged(); - - const check = useCallback(async () => { - try { - const status = await clientRef.current.get('/fofos-admin/status'); - setConfigured(status.configured); - } catch { - // Cannot tell. Render the section rather than the setup form — a transient failure must not look - // like "you have not set this up", which is a much more alarming thing to be told, and would - // invite somebody to re-enter a connection that was never the problem. - setConfigured(true); - } - }, []); - - useEffect(() => { - void check(); - }, [check, signal]); - - if (configured === null) { + if (loading) { return
Loading…
; } /* - Unconfigured, so Administration regardless of what the nav says. The header then NAMES it, which is - the part the overlay could not do: the form used to appear under whichever row happened to be - highlighted, so it read as that section being broken rather than as one step not done yet. + Not ready, so Administration regardless of what the URL asked for. The header then NAMES it, which + is the part the old overlay could not do: the form used to appear under whichever row happened to + be highlighted, so it read as that section being broken rather than as one step not done yet. */ - const current = sectionFor(configured ? selected : 'administration'); + const current = sectionFor(ready ? selected : 'administration'); const Panel = current.built ? PANELS[current.key] : undefined; diff --git a/web/NavPanel.tsx b/web/NavPanel.tsx index 115c41d..f7992d0 100644 --- a/web/NavPanel.tsx +++ b/web/NavPanel.tsx @@ -1,5 +1,7 @@ import { Link, useSearchParams } from 'react-router'; +import { Lock } from 'lucide-react'; import { DEFAULT_SECTION, SECTIONS, isSectionKey, type SectionKey } from './sections'; +import { useReady } from './useReady'; /** * The left column: which part of the fofos site the detail panel is showing. @@ -20,6 +22,15 @@ import { DEFAULT_SECTION, SECTIONS, isSectionKey, type SectionKey } from './sect * `` rather than `` because active state here is a QUERY parameter and NavLink's * `isActive` compares paths — every section would light up at once. So the active class is computed * from the same value the detail panel reads, which is the one place it can come from. + * + * ── Locked until `useReady` ── + * + * Until the plugin is ready, the site's sections are not links at all — a `
`, dimmed, with no + * href to cmd-click and nothing to focus. Rendering a `` and swallowing the click would leave a + * row that looks navigable, offers a URL on hover and opens in a new tab perfectly happily. + * + * The nav only declines to OFFER the door. `DetailPanel` holds the lock, because a typed URL never + * comes through here. */ /** @@ -40,22 +51,20 @@ export const NavPanel = () => { const [params] = useSearchParams(); const raw = params.get('section'); const active: SectionKey = isSectionKey(raw) ? raw : DEFAULT_SECTION; + const { ready } = useReady(); const renderRow = (section: (typeof SECTIONS)[number]) => { const Icon = section.icon; + // Administration is never locked — it is the way OUT of being locked. + const locked = !ready && !section.pinned; // Everything else in the URL is preserved. This is one screen of a workspace, and the global // query string carries state that belongs to the shell rather than to this panel. const search = new URLSearchParams(params); search.set('section', section.key); - return ( - + const body = ( + <> @@ -68,6 +77,26 @@ export const NavPanel = () => { {section.hint} + {locked && } + + ); + + if (locked) { + return ( +
+ {body} +
+ ); + } + + return ( + + {body} ); }; @@ -79,6 +108,9 @@ export const NavPanel = () => { {/* Separated by the same border the rows use, so it reads as the end of the panel rather than as one more page of the site. `border-t` and not `border-t-2`: it is a divider, not a heading. */}
+ {/* Said once, here, rather than on nine dimmed rows. Without it a locked nav reads as broken + rather than as one step not done — the rows go grey and nothing says why or what to do. */} + {!ready &&

Connect fofos below to open the rest.

} {SECTIONS.filter((section) => section.pinned).map(renderRow)}
diff --git a/web/useReady.ts b/web/useReady.ts new file mode 100644 index 0000000..58d0b0e --- /dev/null +++ b/web/useReady.ts @@ -0,0 +1,82 @@ +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. + * + * ── Hardcoded false, deliberately, for now ── + * + * 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. + * + * ── 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. + * + * 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. + */ + +type Status = { configured: boolean; lastProbeOk: boolean | null }; + +export type Readiness = { + /** True only when every section may be opened. */ + ready: 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. + */ + setStatus({ configured: true, lastProbeOk: null }); + } + }, []); + + useEffect(() => { + void check(); + }, [check, signal]); + + // ──────────────────────────────────────────────────────────────────────────────────────────────── + // The gate. Replace `false` with the real condition — nothing else here has to change. + // ──────────────────────────────────────────────────────────────────────────────────────────────── + const ready = false; + + return { ready, loading: status === null }; +};