Media moves to the foot, and each section asks its own question

TWO REPORTS, AND THE SECOND ONE WAS A DESIGN MISTAKE RATHER THAN A SETTING.

Media is pinned now, beside Administration. it sat at the end of the scrolling list,
which is the bottom of the LIST and not the bottom of the panel. it belongs in the
foot for the same reason Administration does: both are about this machine, while
everything above them is about the site's pages.

and it was LOCKED BEHIND A CONNECTION IT NEVER USES. `ready` was one global flag —
"can anything be opened" — and Media is a folder on this disk that never speaks to
fofos. so sections now declare what they need:

  'connection'  the site's pages, which read fofos
  'root'        Media, which needs a folder and nothing else
  'nothing'     Administration, because locking the door you fix things through is
                locking somebody out entirely

`sectionOpen` is one function, used by the nav to decide whether to offer a link and
by the detail panel to decide what to render — so the two cannot disagree about a
section. that was already the rule with one flag; it matters more now that each
section asks a different question.

`/status` gained `rootSet`, a BOOLEAN, and stays not-owner-only. the nav needs the
answer for every account that can see the section; the path itself stays behind
`/_config`, which is owner-only. whether a root has been chosen reveals nothing
about the machine — where it is would.

the empty-state line follows too: "set a root location below to begin" before there
is one, "connect fofos to open the rest" after — rather than one sentence naming a
step that is not the next one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-29 08:44:11 +01:00
co-authored by Claude Opus 5
parent 944d3d9c5b
commit c38d269faf
6 changed files with 97 additions and 14 deletions
+50
View File
@@ -49,6 +49,18 @@ export type Section = {
/** The fofos path this reads, relative to the plugin's mount. Null when nothing is built. */
path: string | null;
built: boolean;
/**
* What has to be true before this section can be opened.
*
* 'connection' the fofos API must be reachable — every section that READS the site
* 'root' only the root location must be set — the file browser needs nowhere to talk to
* 'nothing' always open, because it is where you go to satisfy the other two
*
* This exists because the sections do not all depend on the same thing, and treating them as if they
* did was wrong in a way that showed: Media needs a folder on this machine and nothing else, and was
* locked behind a connection it never uses.
*/
requires: 'connection' | 'root' | 'nothing';
/**
* Pinned to the FOOT of the nav rather than sitting in the list.
*
@@ -68,6 +80,7 @@ export const SECTIONS: Section[] = [
hint: 'The hero, the intro, and the gallery',
path: '/fofos-admin/home',
built: false,
requires: 'connection',
},
{
key: 'about',
@@ -76,6 +89,7 @@ export const SECTIONS: Section[] = [
hint: 'The header photograph and the copy',
path: '/fofos-admin/about',
built: false,
requires: 'connection',
},
{
key: 'collections',
@@ -84,6 +98,7 @@ export const SECTIONS: Section[] = [
hint: 'Collections, and the items in them',
path: '/fofos-admin/collections',
built: false,
requires: 'connection',
},
{
key: 'gallery',
@@ -92,6 +107,7 @@ export const SECTIONS: Section[] = [
hint: 'Standalone pieces',
path: '/fofos-admin/gallery',
built: false,
requires: 'connection',
},
{
key: 'collabs',
@@ -100,6 +116,7 @@ export const SECTIONS: Section[] = [
hint: 'Projects with other people',
path: '/fofos-admin/collabs',
built: false,
requires: 'connection',
},
{
key: 'drops',
@@ -108,6 +125,7 @@ export const SECTIONS: Section[] = [
hint: 'Releases',
path: '/fofos-admin/drops',
built: false,
requires: 'connection',
},
{
key: 'services',
@@ -116,6 +134,7 @@ export const SECTIONS: Section[] = [
hint: 'A fixed tree — the words, not the shape',
path: '/fofos-admin/services',
built: false,
requires: 'connection',
},
{
key: 'site',
@@ -124,6 +143,7 @@ export const SECTIONS: Section[] = [
hint: 'Partners, social links, contact details',
path: '/fofos-admin/site',
built: false,
requires: 'connection',
},
{
key: 'messages',
@@ -132,6 +152,7 @@ export const SECTIONS: Section[] = [
hint: 'What people sent through the contact form',
path: '/fofos-admin/messages',
built: true,
requires: 'connection',
},
{
/*
@@ -151,6 +172,10 @@ export const SECTIONS: Section[] = [
hint: 'Photos and video the site is built from',
path: null,
built: true,
// A folder on this machine. It never speaks to fofos, so a connection is not its business.
requires: 'root',
// Pinned beside Administration: both are about this machine rather than about the site's pages.
pinned: true,
},
{
/*
@@ -164,6 +189,9 @@ export const SECTIONS: Section[] = [
hint: 'Where fofos is, and the token to reach it',
path: null,
built: true,
// Always reachable. It is where the other two requirements are satisfied, so locking it would be
// locking somebody out of the only door.
requires: 'nothing',
pinned: true,
},
];
@@ -176,3 +204,25 @@ export const isSectionKey = (value: string | null): value is SectionKey =>
export const sectionFor = (key: SectionKey): Section =>
SECTIONS.find((section) => section.key === key) ?? SECTIONS[SECTIONS.length - 1]!;
/**
* Can this section be opened, given what is currently true?
*
* One function, used by the nav to decide whether to offer a link and by the detail panel to decide
* what to render — so the two cannot disagree about a section. That was already the rule when there was
* one global flag; it matters more now that each section asks its own question.
*/
export const sectionOpen = (section: Section, state: { ready: boolean; rootSet: boolean }): boolean => {
switch (section.requires) {
case 'nothing':
return true;
case 'root':
return state.rootSet;
case 'connection':
return state.ready;
}
};
/** Why it is locked, in the fewest words that are actually useful. */
export const sectionBlockedBy = (section: Section): string =>
section.requires === 'root' ? 'needs a root location' : 'needs a connection to fofos';