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;
+}) => (
+
+
+
+ {/* 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.
-