diff --git a/manifest.ts b/manifest.ts index 19020c7..0d997a6 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.04', + version: '2026.08.29.05', platform: '>=1.0.0', label: 'Fofos', diff --git a/web/AdministrationPanel.tsx b/web/AdministrationPanel.tsx new file mode 100644 index 0000000..9ae1c2f --- /dev/null +++ b/web/AdministrationPanel.tsx @@ -0,0 +1,55 @@ +import { Database } from 'lucide-react'; + +/** + * Administration — this plugin's own machinery, not the client's site. + * + * ── It is a list of blocks, and it starts with one ── + * + * Not one form. Administration will hold several unrelated settings, and giving each its own titled + * block from the start means adding the second one moves nothing — as against a single form that has + * to be broken up the first time anything joins it. + * + * ── What used to be here ── + * + * An address and a token: where fofos is, over HTTP. That is gone. It has NOT been replaced yet, so + * nothing can currently configure this plugin — which is why `useReady` stays hardcoded false and + * every other section stays locked. The two are the same change and neither is finished. + * + * The server half is untouched: `/_config` still stores a URL and a token, and the proxy in + * `api/router.ts` still reads them. Only the way in from the screen is gone. + */ + +/** One titled block. The shape every Administration setting gets, so the second one costs nothing. */ +const Block = ({ + icon: Icon, + title, + hint, + children, +}: { + icon: typeof Database; + title: string; + hint: string; + children?: React.ReactNode; +}) => ( +
+
+

+ + {title} +

+

{hint}

+
+ {children} +
+); + +export const AdministrationPanel = () => ( +
+ + {/* Deliberately empty. The block exists so the shape is settled and so the screen says what is + coming rather than looking like a section nobody finished — but nothing is decided about what + goes in it, and guessing at fields would be a decision made in the wrong place. */} +

Nothing to set yet.

+
+
+); diff --git a/web/ConnectionPanel.tsx b/web/ConnectionPanel.tsx deleted file mode 100644 index 53cc12f..0000000 --- a/web/ConnectionPanel.tsx +++ /dev/null @@ -1,240 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; -import { CheckCircle2, Loader2, Plug, ShieldAlert, Trash2, TriangleAlert } from 'lucide-react'; -import { useClient } from 'hooks/useClient'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { readableError } from './errors'; -import { useFofosChanged } from './channel'; - -/** - * Where fofos is, and the token to reach it. Both the first-run setup and the permanent settings. - * - * One component for both — the same choice `plugins/proxy-manager/web/ConnectionPanel.tsx` makes, and - * for the same reason: a connection edited months later goes through exactly the code path that - * stored the first one, so there is no second path to rot. - * - * ── Owner only, and it says so rather than failing oddly ── - * - * The manifest marks `/_config` `ownerOnly`, so every request from this component 403s for anybody - * else — including the test button, deliberately, because that request carries the token in its body. - * A member who reaches this screen should read a sentence explaining that, not a raw refusal. - * - * ── The token is never loaded ── - * - * `GET /_config` returns `hasToken`, never the token. So the field starts empty even when one is - * stored, and an empty field on save means "keep the stored one" — which is what lets a typo in the - * URL be fixed without going to find the credential again. - */ - -type ConnectionView = { - configured: boolean; - url: string | null; - hasToken: boolean; - sections: string[] | null; - lastProbeAt: string | null; - lastProbeOk: boolean | null; -}; - -type TestResult = { - reachable: boolean; - authenticated: boolean; - sections?: string[]; - error?: string; -}; - -export const ConnectionPanel = () => { - const client = useClient(); - const clientRef = useRef(client); - clientRef.current = client; - - const [view, setView] = useState(null); - const [url, setUrl] = useState(''); - const [token, setToken] = useState(''); - - const [busy, setBusy] = useState(false); - const [result, setResult] = useState(null); - const [error, setError] = useState(null); - const [forbidden, setForbidden] = useState(false); - - const [, bump] = useFofosChanged(); - - const load = useCallback(async () => { - try { - const current = await clientRef.current.get('/fofos-admin/_config'); - setView(current); - setUrl(current.url ?? ''); - } catch (failure) { - // A 403 here is not a fault — it is a member looking at an owner-only screen, and it needs a - // different sentence from "that did not work". - if ((failure as { status?: number })?.status === 403) setForbidden(true); - else setError(readableError(failure)); - setView({ configured: false, url: null, hasToken: false, sections: null, lastProbeAt: null, lastProbeOk: null }); - } - }, []); - - useEffect(() => { - void load(); - }, [load]); - - const test = async () => { - setBusy(true); - setError(null); - try { - // Sent only when typed. Omitted, the server tests what is STORED — which is how "is my - // connection still good?" is answered without retyping a secret. - const body: Record = {}; - if (url.trim()) body.url = url.trim(); - if (token.trim()) body.token = token.trim(); - - setResult(await clientRef.current.post('/fofos-admin/_config/test', body)); - } catch (failure) { - setError(readableError(failure)); - } finally { - setBusy(false); - } - }; - - const save = async () => { - setBusy(true); - setError(null); - try { - const body: Record = { url: url.trim() }; - if (token.trim()) body.token = token.trim(); - - const saved = await clientRef.current.put( - '/fofos-admin/_config', - body, - ); - setView(saved); - setResult(saved.probe ?? null); - // Cleared on success: it is stored now, and keeping it on screen serves nobody. - setToken(''); - // Everything else re-reads. Saving a connection is the one action that changes what every other - // panel can see. - bump(); - } catch (failure) { - setError(readableError(failure)); - } finally { - setBusy(false); - } - }; - - const forget = async () => { - setBusy(true); - try { - setView(await clientRef.current.delete('/fofos-admin/_config')); - setUrl(''); - setToken(''); - setResult(null); - bump(); - } catch (failure) { - setError(readableError(failure)); - } finally { - setBusy(false); - } - }; - - if (forbidden) { - return ( -
- -

Only the owner can change this connection.

-

- The token stored here can rewrite the whole public site, so setting it belongs to whoever owns the - server. You can still edit content. -

-
- ); - } - - if (!view) { - return
Loading…
; - } - - return ( -
-
-

- - The fofos server -

-

- Where the site lives, and a token to edit it. The token is fofos's FOFOS_API_TOKEN. -

-
- - - - - -
- - - {view.configured && ( - - )} -
- - {error &&

{error}

} - - {result && ( -
- {result.authenticated ? ( - - ) : ( - - )} - - {result.authenticated - ? `Connected. ${result.sections?.length ?? 0} sections available.` - : (result.error ?? 'That did not work.')} - -
- )} - - {view.configured && !result && ( -

- {view.lastProbeOk === null - ? 'Saved, and not yet checked.' - : view.lastProbeOk - ? `Last checked ${view.lastProbeAt ? new Date(view.lastProbeAt).toLocaleString() : ''} — working.` - : 'The last check failed. Press Test to try again.'} -

- )} -
- ); -}; diff --git a/web/DetailPanel.tsx b/web/DetailPanel.tsx index 8d29f4f..6fc712d 100644 --- a/web/DetailPanel.tsx +++ b/web/DetailPanel.tsx @@ -1,6 +1,6 @@ import { useSearchParams } from 'react-router'; import { DEFAULT_SECTION, isSectionKey, sectionFor, type SectionKey } from './sections'; -import { ConnectionPanel } from './ConnectionPanel'; +import { AdministrationPanel } from './AdministrationPanel'; import { MessagesPanel } from './MessagesPanel'; import { PlannedSection } from './PlannedSection'; import { useReady } from './useReady'; @@ -38,7 +38,7 @@ import { useReady } from './useReady'; */ const PANELS: Partial React.JSX.Element>> = { messages: MessagesPanel, - administration: ConnectionPanel, + administration: AdministrationPanel, }; export const DetailPanel = () => {