import { useCallback, useEffect, useRef, useState } from 'react'; import { Check, Loader2, RotateCcw } 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 { DirectoryPicker } from './DirectoryPicker'; /** * Where the fofos site is on this machine. * * ── The default is a suggestion until it is saved ── * * An unset root reports `/fofos` and says so. It is not written to the database by being * looked at — a value somebody has never agreed to must not become one they appear to have chosen, and * a default that follows the machine is more useful than one frozen on install day. * * ── Typing and picking are the same field ── * * The picker fills the field; the field is what saves. So there is one value on screen and one Save, * and picking a folder never commits anything by itself — the path is visible and editable before it * is stored. It also means a location OUTSIDE the owner's home can still be typed, which the picker * cannot reach. */ type Settings = { rootPath: string; isSet: boolean }; export const RootLocationBlock = () => { const client = useClient(); const clientRef = useRef(client); clientRef.current = client; const [saved, setSaved] = useState(null); const [value, setValue] = useState(''); const [picking, setPicking] = useState(false); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [forbidden, setForbidden] = useState(false); const load = useCallback(async () => { try { const current = await clientRef.current.get('/fofos-admin/_config/root'); setSaved(current); setValue(current.rootPath); } catch (failure) { // A member looking at an owner-only setting is not a fault and needs a different sentence. if ((failure as { status?: number })?.status === 403) setForbidden(true); else setError(readableError(failure)); } }, []); useEffect(() => { void load(); }, [load]); const save = async () => { setBusy(true); setError(null); try { const result = await clientRef.current.put('/fofos-admin/_config/root', { path: value }); setSaved(result); // From the server, not from what was typed — `~` is expanded and trailing slashes go, so the // field must show what was actually stored rather than what was sent. setValue(result.rootPath); } catch (failure) { setError(readableError(failure)); } finally { setBusy(false); } }; const reset = async () => { setBusy(true); setError(null); try { const result = await clientRef.current.delete('/fofos-admin/_config/root'); setSaved(result); setValue(result.rootPath); } catch (failure) { setError(readableError(failure)); } finally { setBusy(false); } }; if (forbidden) { return

Only the owner can change where the site lives.

; } if (!saved) { return

Loading…

; } const dirty = value.trim() !== saved.rootPath; return (
setValue(event.target.value)} placeholder="/home/you/fofos" className="min-w-0 flex-1 font-mono text-xs" autoComplete="off" spellCheck={false} /> {saved.isSet && ( )}
{picking && ( setPicking(false)} onPick={(absolutePath) => { setValue(absolutePath); setPicking(false); }} /> )} {error &&

{error}

} {/* Says which of the two states this is in. "Not set" reading identically to "set to exactly the default" is how somebody comes to believe they configured something they did not. */}

{saved.isSet ? 'Saved.' : 'Not set yet — this is the default until you save it.'}

); };