diff --git a/api/router.ts b/api/router.ts index 846cc83..2c8332c 100644 --- a/api/router.ts +++ b/api/router.ts @@ -2,7 +2,7 @@ import type { Context } from 'hono'; import { createRouter, type HonoVariables } from '@@/create-router'; import { registerProxiedPrefix } from '@@/sidecar/proxied-prefixes'; import { deleteConnection, readConnection, readCredentials, recordProbe, saveConnection } from './connection'; -import { readSettings, saveRootPath, clearRootPath } from './settings'; +import { readSettings, saveRootPath } from './settings'; import { fofosRequest, FofosError, probe, type FofosResponse } from './fofos-client'; /** @@ -180,11 +180,6 @@ router.put('/_config/root', async (ctx) => { } }); -router.delete('/_config/root', async (ctx) => { - const user = ctx.get('user'); - return ctx.json(await clearRootPath(user.email)); -}); - /** * Is this plugin usable? Asked once by the detail panel rather than by all nine sections. * diff --git a/api/settings.ts b/api/settings.ts index ba04e5e..f77c365 100644 --- a/api/settings.ts +++ b/api/settings.ts @@ -14,7 +14,9 @@ import { fofosAdminSettings } from '../db/schema'; * restore onto a machine with a different owner then points at a directory that does not exist while * looking deliberately chosen. Computed, it follows the machine; written, it follows the install. * - * A saved path is a DECISION and is stored exactly as given. Only the absence of one is derived. + * A saved path is a DECISION and is stored exactly as given. Only the absence of one is derived — and + * once saved it stays saved: there is no route back to the default, because "reset this to something + * you never chose" is not a thing anybody asked for and it read as a second, quieter Save. * * ── Absolute, always ── * @@ -81,15 +83,3 @@ export async function saveRootPath(input: string, email: string): Promise { - const existing = await row(); - if (existing) { - await db - .update(fofosAdminSettings) - .set({ rootPath: null, updatedAt: new Date() }) - .where(eq(fofosAdminSettings.id, existing.id)); - } - return readSettings(email); -} diff --git a/manifest.ts b/manifest.ts index 4222128..737500f 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.07', + version: '2026.08.29.08', platform: '>=1.0.0', label: 'Fofos', @@ -52,7 +52,7 @@ export const manifest: PluginManifest = { { key: 'fofos-admin', label: 'Fofos', - description: "Edit the fofos website — pages, galleries, drops, collabs and the messages people send", + description: 'Edit the fofos website — pages, galleries, drops, collabs and the messages people send', // Where the fofos URL and token are set, and the probe that checks them. Prefix-matched, so // `/_config/test` is covered too — deliberately, because that request carries the token in its diff --git a/web/RootLocationBlock.tsx b/web/RootLocationBlock.tsx index 5b245ed..aa8626d 100644 --- a/web/RootLocationBlock.tsx +++ b/web/RootLocationBlock.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useRef, useState } from 'react'; -import { Check, Loader2, RotateCcw } from 'lucide-react'; +import { Check, Loader2, Pencil } from 'lucide-react'; import { useClient } from 'hooks/useClient'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -21,6 +21,17 @@ import { DirectoryPicker } from './DirectoryPicker'; * 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. + * + * ── Two states, and the actions are on their own row ── + * + * Reading, it is a path and one Edit. Editing, it is a field and the things you can do to it. The + * actions sit on the block's BOTTOM row rather than beside the value, so the path gets the full width + * — it is long, absolute and the thing somebody came here to read, and it was being truncated to make + * room for buttons that are not needed most of the time. + * + * Edit is a mode rather than an always-live field because this is a setting that is looked at far more + * often than it is changed, and a live input invites a stray keystroke into a path that everything + * else depends on. */ type Settings = { rootPath: string; isSet: boolean }; @@ -32,6 +43,7 @@ export const RootLocationBlock = () => { const [saved, setSaved] = useState(null); const [value, setValue] = useState(''); + const [editing, setEditing] = useState(false); const [picking, setPicking] = useState(false); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); @@ -62,20 +74,8 @@ export const RootLocationBlock = () => { // 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); + setEditing(false); + setPicking(false); } catch (failure) { setError(readableError(failure)); } finally { @@ -93,32 +93,36 @@ export const RootLocationBlock = () => { const dirty = value.trim() !== saved.rootPath; + const stopEditing = () => { + // Back to what is stored, so leaving the mode cannot leave a half-typed path on screen looking + // like the setting. + setValue(saved.rootPath); + setEditing(false); + setPicking(false); + setError(null); + }; + return (
-
+ {editing ? ( setValue(event.target.value)} placeholder="/home/you/fofos" - className="min-w-0 flex-1 font-mono text-xs" + className="w-full font-mono text-xs" autoComplete="off" spellCheck={false} /> - - - {saved.isSet && ( - - )} -
+ ) : ( + // `break-all`, not `truncate`. An absolute path is read from BOTH ends — the machine at the + // front and the folder at the back — so hiding either one is worse than wrapping. +

+ {saved.rootPath} +

+ )} - {picking && ( + {editing && picking && ( setPicking(false)} onPick={(absolutePath) => { @@ -135,6 +139,30 @@ export const RootLocationBlock = () => {

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

+ + {/* The block's bottom row. Everything you can DO is here, on its own line, so the path above it + gets the full width. */} +
+ {editing ? ( + <> + + + + + ) : ( + + )} +
); };