32 Commits
Author SHA1 Message Date
pastilhasandClaude Opus 5 9ca6077e2c Collabs, as a third configuration of one panel
The same object as a collection and a gallery piece, so it is a config in
subjects.ts and a one-line panel — no new screen.

Two differences, both deliberate. It has no SPECS: a collab is a project rather
than an object, so there is no measurement or raw material and the form is a
name, an address and a description. And its title column is `project_name`,
which is the client's word and stays — the site's API publishes both that and
`title` so this panel reads one field like every other section, and the
translation lives in one router rather than as a branch in every screen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 13:42:26 +01:00
pastilhasandClaude Opus 5 6e33e89e2c finish the carousel rename in the panels
Two of these were live: PageBuilderPanel still compared kind === 'gallery' in
two places, so a carousel block's editor and its Choose button were both dead
branches. The type checker could not see them — both compare against string.

The rest is copy somebody reads: "Change gallery", "Name a new gallery",
"Reading the gallery", "Add to this gallery", Home's Gallery heading, and the
"Create Gallery" file action, now "Carousel from folder".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 18:06:06 +01:00
pastilhasandClaude Opus 5 15042ccc76 a gallery is a carousel
It always rendered as one — data-carousel, a snap track, two arrows, driven by
carousel.client.js. The entity was the last thing still calling it something
else, and that collided with the site's own Gallery section: two nav rows
reading Gallery and Galleries, meaning a list of objects and a list of
photograph sets.

GalleriesPanel, GalleryPicker and GalleryItems become Carousels*, the endpoint
becomes /fofos-admin/carousels, the block kind becomes carousel and
subject-gallery becomes subject-carousel. The publish allowlist follows the
renamed tables.

What did NOT move: GALLERY_CONFIG at /fofos-admin/gallery, subjects.ts's
base '/gallery', and home_gallery_media. Those are the Gallery section and the
legacy rows, and both are correct.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 18:01:15 +01:00
pastilhasandClaude Opus 5 5e5c06e76e Pieces: the Gallery, as a section like Collections
The Gallery is a flat list of peças, each with a title, a gallery of mixed
media, and the specs from the client's sitemap — measurement, raw material,
colours, and NOTAS: production time and shipping conditions.

That is a collection one level flatter, so the panel is shared rather than
copied. SubjectPanel and SubjectFields take a section config (subjects.ts);
CollectionsPanel and PiecesPanel are one line each.

The five template blocks are renamed collection-* to subject-*. They never read
anything collection-specific, and copying them per section would have meant
twenty kinds by the time Drops and Collabs land.

Called Pieces in the nav, not Gallery — there is already a Galleries section
meaning a named set of media, and two rows called Gallery and Galleries is a
question somebody has to answer every time they look at it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 17:44:32 +01:00
pastilhasandClaude Opus 5 60eb6ecfc9 a place to manage each collection
Collections, above Galleries and Media, where the other things made of media already are.

There is no Create button and no Delete, and that is the design rather than an omission. Which
collections exist is a question about `originals/COLLECTIONS/`, so making one means making a folder —
in Media or in Finder, and both work. A Delete here would either drop the row and leave the
photographs, so the collection reappears on the next read, or delete somebody's pictures. Neither is
what Delete should mean in an admin.

The grid is for choosing the MAIN photo and nothing else. The photographs are already in filename
order and the gallery is the folder, so there is nothing to reorder and nothing to add — the one
decision is which picture leads. Pressing the chosen one again clears it, which is how "no choice, use
the first" stays reachable without a second control that only does that.

The badge says Main rather than Cover because it has to be true of the first photograph, which leads
by default, as well as of one somebody picked.

The fields are the other half — everything a directory cannot tell you: the name it goes by, the
address, and the specs from the client's sitemap. `folder` is shown and not editable, and the API
refuses to write it: it is an address, and renaming one moves files. Shown anyway, because it is the
only thing that says WHICH folder a collection is.

One draft, one save. Per-field writes would be eight requests for somebody filling in a form, and the
last to land would win in an order nobody controls.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 14:03:54 +01:00
pastilhasandClaude Opus 5 ca023eb651 pages reorder by dragging; the list panel is gone
The grip drags and the row is the drop target — a Link is draggable by default
and drags its URL, so making the row itself draggable fights the browser for what
the gesture means.

The list rearranges as the pointer crosses a row rather than on drop, and the
write happens once at the end: PUT /pages/order takes the whole set, because an
order is a property of the set.

/fofos-admin/pages was a list of cards beside a nav that already lists the pages.
Two lists of the same thing, and the one you were looking at was never the one
you had clicked. It redirects to the first page now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 09:02:02 +01:00
pastilhasandClaude Opus 5 e45e3d84a8 the bar is gone; the device toggle is the editor's
It sat across the top of the detail panel with Sync, Build and Publish — three
things that act on the site, beside one that decides how you are looking at it.
The toggle belongs with the settings it scopes: a spacing is chosen per width,
so the control saying which width is the first thing in that column.

Sync, Build and Publish are unmounted until they have a place of their own.
ViewModeProvider stays; it was never the bar's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 07:38:43 +01:00
pastilhasandClaude Opus 5 b5ca67e68e every control writes itself
The Save button is gone, and so is the SaveTarget context that announced it.
Every screen here is a preview of the thing being edited, so a change is already
visible before it is written — a button then asks somebody to confirm what they
can see.

useAutosave debounces a burst into one request. It takes the unsaved state as a
string rather than a flag, so a failed save is not retried on the next render,
and it flushes on unmount so a section change loses nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 07:34:34 +01:00
pastilhasandClaude Opus 5 a7fc07c52f sections are paths, not a query string
The admin declares its routes in the manifest, so /fofos-admin/layout/header and
/fofos-admin/pages/home are real URLs. One reader takes the two segments after
the mount; links carry no query string at all, so nothing leaks between
sections.

What stays in the query is how you are looking — device, tab, view — rather than
at what.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 04:57:29 +01:00
pastilhasandClaude Opus 5 852c53dd09 one device control, read from the URL
Spacing's settings are per width, so the section has to say which width it is editing — and the answer
must be the one the preview is already showing, not a second selector able to disagree with it.

It reads `?device=` rather than the ViewMode context, and that is not a preference. The editor is a
SIBLING panel: an element built inside the detail panel resolves context where it is RENDERED, so a
toggle handed over would look up a tree with no provider in it and quietly show the default forever.

The URL is the shared state, which means there is nothing to share. Three components read one
parameter, so they cannot disagree, and the preview follows without being subscribed to anything — it
reads the same value. More editor sections can take the control by rendering it.

The bar's own toggle is now the same component. It had its own copy of the options and the handler,
which is exactly how two controls for one thing start to differ.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 03:53:02 +01:00
pastilhasandClaude Opus 5 8f673792d1 there is no View, only Edit
The bar offered Edit and View, and View existed to see the page without the builder's outlines and
handles on top of it. It was the DEFAULT, which put a click in front of every arrival at the one panel
whose job is editing — and it put a branch in every painter that did nothing but wait for that click.

So the mode is gone rather than defaulted the other way: no toggle, no `?mode=`, no guard. The clean
page is still one click away and always was — it is the site, on its own URL. What the admin owes is
the editing surface, always.

The left group in the bar stays, empty. The middle one is centred against the BAR rather than against
its neighbours, so removing the left group would let Desktop/Tablet/Mobile drift as the right group
changes width.

The preview's caption stays too. It reads as a duplicate of the device buttons and is not: they say the
width, and it says which SERVER — development, not production — which nothing else on the screen does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 19:23:37 +01:00
pastilhasandClaude Opus 5 78db209ea1 Sync moves to the bar, beside Build and Publish
It sat under the two folders it acts on, which was right while it was a detail of setting fofos up. It
is not one: a stale `derived/` is why a picture is missing from a page somebody is looking at, and the
fix was three sections away in a card about where the site lives. Beside Build and Publish it reads as
what it is — the third thing you do to a site, and the one that has to happen first.

The card keeps what needs room: the progress bar, the failures, the run history.

Shared through `useGlobal`, keyed `['fofos-admin','media-sync']` — a slot in the app-wide query cache,
so no provider has to wrap the bar and the panel together and a third reader is one line. Not a
context: this codebase has one for Save because a SECTION owns that state and announces it upward.
Nobody owns this one; it is a fact about a folder, which is what a keyed slot is for.

`useGlobal` shares the value and not the work, so the fetching is a separate hook with exactly one
caller — the button. The bar is mounted whenever the plugin is open and the Administration card is
mounted only sometimes, so the thing that is always there is the thing that asks. Two callers would
poll the same endpoint twice a second.

Gated on `rootSet`, not `ready`: sync reads a folder on this machine and never speaks to fofos.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 11:58:28 +01:00
pastilhasandClaude Opus 5 5b1c5a2590 the URL says which page, not which row
`?page=about` instead of `?page=2`. A URL that names the page can be read, sent to somebody, and still
means the right thing after a database is restored from a dump with different ids — which happened
once in this project already, earlier today.

Home needs a word, because its slug is empty by design: that is the check constraint, and it is what
makes `/` fall out of joining a slug to the root. An empty string is no use in a query parameter that
has to name a page unambiguously, so the admin says `home` — and the site's API now REFUSES that slug,
which is what makes the mapping total in both directions rather than merely usual.

One reserved address costs nothing. Anybody who wanted /home wanted the front page, which they have.

One place maps a page to its URL value and back — pageParam and pageFromParam — so the nav, the detail
panel and the builder cannot disagree about what `?page=` means.

`record` is deleted on every navigation. It belonged to the per-page record panels, which are
unreachable now; nothing reads it, so it was not a bug, but every URL anybody copied carried a
parameter naming a row in a section that no longer exists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 04:42:16 +01:00
pastilhasandClaude Opus 5 a4c77c642f a selected page opens the builder, not a highlighted row
The nav already said it: Pages is a section, and the pages are rows under it. Two things, two screens.
The detail panel was showing one screen with a row lit up, which made the list the answer to both
questions.

  ?section=pages              the list — what exists, what it is called, where it lives, in what order
  ?section=pages&page=4       that page — what is on it

Both addressable, which is why the distinction is a query parameter rather than state.

Pressing PAGES itself now clears `page`. Without that the section row and the row for whichever page
was last open lead to the same screen and there is no way back to the list but the browser's back
button. Scoped exactly like `media`, and for the same reason: a parameter belonging to one section
should not ride along into the others.

The list stops having a selection — selecting a page leaves the screen entirely — so its rows are
plain rows again and the panel no longer reads or writes the URL at all.

Adding a page deliberately does NOT open it. The next thing anybody does is name it, and the name lives
on the list; jumping into the builder would mean coming straight back.

SitePreview is now shared, lifted out of LayoutPanel with the dev-port lookup beside it. Layout passes
the theme it is editing; a page passes nothing and just gets its own page. The builder shows the real
page at its real address today, which is what the blocks will be judged against when they exist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 03:30:44 +01:00
pastilhasandClaude Opus 5 a0f23d70bb Pages, with the pages under it in the nav
The section is real, and the nav draws the site's pages as rows beneath it. That is the one place this
admin has a hierarchy and it earns it: a page is a thing you go to, and the list of them is the site.

Drawn whether or not Pages is the open section — hiding the list until you click makes the nav a menu
of menus.

Three kinds of change, saved three different ways, and the split is deliberate. TEXT — a title, a menu
label, an address — is drafted and written by the one Save in the bar, because typing is something
people do halfway and change their minds about. ADDING, DELETING and REORDERING are immediate: each is
a single deliberate gesture with an obvious result, and holding them in a draft would put a list on
screen that does not exist yet, then leave Save to decide what order to apply three kinds of change in.

Only the rows that actually changed are sent, so a save touches what somebody edited and nothing else.
A failed save reloads from the server rather than leaving the typed values on screen, where they would
look saved.

Home's address and delete button are ABSENT, not disabled. A greyed control invites somebody to work
out why they cannot use it; there is nothing to work out, the front page is always `/`.

The two panels share the list through a channel rather than both fetching it. They are siblings and
neither can pass the other a prop, and two fetches means two lists that agree until somebody renames a
page. Selection stays in the URL — `?page=` — because that is addressable and the list is not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 03:25:03 +01:00
pastilhasandClaude Opus 5 143b34d2d2 Edit/View on the left, three widths in the middle
The bar is three groups now: how you are looking at it, at what width, and what to do with it. The
middle is centred against the BAR rather than against its neighbours, because the outer two change
width as sections come and go and a middle that drifted with them would never be in the same place
twice.

A section OPTS IN, the same way it opts into Save. Most have no preview — Media is a file browser,
Administration is a connection form, Messages is an inbox — and offering to view any of them at tablet
width offers something that means nothing. A control that does nothing teaches people that controls do
nothing.

The widths are REAL viewport sizes and the frame is SCALED to fit, never squeezed into whatever the
panel has. That distinction is the whole point: an iframe given the panel's width has the site's own
media queries answering the PANEL's size, so "Desktop" in a narrow panel renders the tablet layout and
labels it desktop — the one thing a preview must not do. Never scaled up either: a 390px phone blown
up to fill a wide panel is a phone nobody has, and every judgement made against it is wrong in the
direction of "looks fine".

390 is the iPhone width slots.ts already assumes. 834 is iPad portrait, deliberately above the site's
md: breakpoint at 768 so it shows the desktop nav rather than the drawer — 768 exactly sits on the
boundary and is the least informative width available. 1440 is past the 1280 the content column caps
at, so the margins either side are visible rather than implied.

View gives the preview the whole panel. That is what makes it worth having: at edit height a phone is
a letterbox, and a phone is the width somebody most wants to look at properly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 02:26:06 +01:00
pastilhasandClaude Opus 5 09b914c430 Layout replaces one section per page
The admin was a section each for Home, About Us, Collections, Gallery, Collabs, Drops and Services,
because the site's pages were a constant in site-map.ts and a developer was the only one who could add
one. A page builder is going to replace that, so the nav stops enumerating pages.

What is left is the two things a page is NOT: Layout — the shell every page renders inside — and Pages,
listed but not built. Layout is first because it is first in the work: building pages before the frame
they sit in means building them twice.

The panel is a sketch of the page with the controls under it. Menu across the top, a body, the footer
at the bottom, in the chosen colours and typefaces — the same principle the header editor already
follows, where the admin renders the structure as the page will show it. It is honest about being a
sketch: drawing the logo and the rules would be a second implementation of the layout, and the two
would drift the first time either changed.

Fonts are a mapping, not free text. styles.css already declares four ROLES — body, sub, title,
display — and every template asks for a role rather than a typeface, so choosing a font is choosing
which of the four faces the build subsets plays each part. The list travels with the payload rather
than being copied here, because a picker offering a face the build never subsets renders as a fallback
nobody chose.

The per-page panels are NOT deleted. AboutPanel, HomePanel and RecordsPanel are still in the tree and
still work; they are unreachable, which is a line in sections.ts rather than a rewrite. The page
builder is not finished, and removing the only way to edit the live site's content before its
replacement exists would leave a gap with nothing in it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 01:38:05 +01:00
pastilhasandClaude Opus 5 abdb519e8b one Save, in the panel's bar
Every section had its own Save buttons — four on About, two on Home, one per record — and on a page
long enough to scroll, the button you wanted was the one that had just left the screen.

Now there is one, beside Publish, pinned above the scroll. The section announces whether it has unsaved
changes, whether it is busy, and how to save; the bar renders that. Neither knows what the other is,
which is what keeps DetailPanel a map of keys to components rather than a place that has to know what
each section can do.

── What this replaces ──

The per-card saves existed because the API treats an absent key as "leave it alone", so a Save carrying
everything meant a half-written manifesto shipping with a photograph somebody meant to change. With one
Save that is the model rather than an accident: one press writes the page you are looking at.

The cost, stated plainly: there is no longer a way to commit one part of a page and leave the rest.
Nothing here needed that, and if something ever does it wants its own verb rather than four buttons all
called Save.

── Dirty is a comparison, not a flag ──

Against what the server last returned, field by field. A flag says "something was typed" and stays true
after you undo it by hand; a comparison says "this differs from what is stored", which is the question
a Save button is actually answering — and it is what lets the button be dead when there is nothing to
do, which is the whole value of having one.

The gallery picker on Home is deliberately outside it and still writes on selection: a <select> that
needs a second press to mean anything is a control people believe they have already used.

Sections with nothing to save render no button rather than a dead one. A permanently disabled control
invites a press and teaches that pressing does nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 23:22:35 +01:00
pastilhasandClaude Opus 5 e8ce6bc830 Build and Publish get their own bar, pinned above the scroll
They were in the workspace's chrome header for an afternoon. Two things were wrong with that.

That header is shared furniture — every panel in the app draws it the same way — and these two buttons
belong to this plugin and to the site it administers, not to the frame around it.

And the panels scroll their own contents now. Anything drawn inside one leaves the screen as soon as
somebody reads to the bottom, which is exactly when they reach for Publish.

So the bar is a sibling of the scrolling region rather than the first thing inside it: `shrink-0` above
a `min-h-0 flex-1`, which is the same arrangement that makes the panels scroll at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 23:18:19 +01:00
pastilhasandClaude Opus 5 0dce8fcb3c About Us
Four bands in the order the page shows them, which is the sitemap's shape exactly: a HEADER of a
photograph and a phrase, then ABOUT US, then PRODUTO with a video beside its text, then MANIFESTO.

No schema change and no API change — `about_content` already carries every one of those columns and
`aboutRouter` already reads and writes them. This is the screen that was missing, not the data.

One save per band, the rule Home follows and for the same reason: `take()` treats an absent key as
"leave it alone", so a single Save carrying every field would let a half-finished manifesto ride along
with a photograph somebody meant to change.

Both media slots go through MediaPicker rather than a text field. A path somebody types is a path that
is wrong by one character and renders as a broken image. One picker serves both, with `picking` naming
which slot asked — two would be two copies of a modal that already knows how to browse originals/.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 22:33:45 +01:00
pastilhasandClaude Opus 5 ccf5768a3d Gallery, Drops and Collabs
Three sections, one panel, configured — the same split the site's API already makes. Each is a titled
record with an ordered list of media hanging off it, and the only real differences are what the title
is called and which extra fields the record carries.

The htmx admin had three screens and that was defensible there, because each carried its own markup as
well. There is no markup to differ here, so three panels would be three places to fix one ordering bug.

The `fields` list in records.ts is the FORM for what the server allows, not a second authority over it.
What may actually be written is decided by `valuesFrom` in the site's sections.ts, per section, as an
allowlist — a key added here that the server does not accept is an input that silently saves nothing.

Which record is open lives in `?record=<id>` beside `?section=`, per docs/navigation-audit.md, and the
detail is loaded from that id rather than carried over from the list. That is what makes a typed or
reloaded URL work, and it is guarded against a slow response for a record nobody is looking at any
more overwriting a fast one for the record they opened next.

Ordering sends every id in the order it should be, so two items cannot claim one position. Media is
added one path at a time because the server appends, and a parallel add would land in whatever order
the requests happened to finish.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 00:40:45 +01:00
pastilhasandClaude Opus 5 f61653f7af Galleries: a panel, and Create Gallery in the file browser
The section reads the site's own admin API, so it requires a CONNECTION where
Media beside it requires only a root. The difference is real rather than
copy-paste: Media is a file browser over a folder on this disk, while a gallery
is a row in the site's database — opening it without the site answering would be
a screen where every action fails.

The context action hands its selection over in the URL and sends you to the
panel rather than opening a dialog of its own. Naming a gallery, seeing the slug
it will get and watching the items land belongs on the screen that owns
galleries, not in a modal over a file browser that would then have to explain
where the result went.

It offers itself only inside originals/, because a gallery item is a path
relative to that folder and an entry that appears everywhere while working in one
place is worse than one that appears where it works. The test is on the PATH, not
on which panel is open — the same browser is mounted in several places and only
the path says where it is pointed.

A mixed selection is refused. A folder among files is either a mistake or a
request to recurse, and neither is something to guess from a right-click.

The handoff is consumed once and stripped from the URL. Left there, a refresh
would create the gallery again, which somebody discovers as four copies.

Nothing is written until Create: the folder's name is a suggestion in the field,
not a decision already taken.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 21:08:25 +01:00
pastilhasandClaude Opus 5 3ae8c960e5 the Home section, built against the site's own admin API
Hero, intro and an ordered gallery — the shape the old htmx admin had, as a
panel here.

Every write returns the WHOLE payload and this never patches its own state from
what it sent. That matters more than it looks: the server clears the hero crop
when the hero changes, because a rectangle describing a region of a different
picture would crop the new one somewhere arbitrary. A client keeping its own
copy would go on showing a framing the server had already discarded.

Ordering sends every id rather than one item's position, which is what makes it
impossible to end up with two things claiming the same place.

Thumbnails load through this plugin's proxy rather than straight from the dev
server, so they arrive with the session instead of requiring the browser to
reach a port on the host.

It edits DEVELOPMENT, because production is a directory of static files with no
API to write to. An edit lands on dev immediately and becomes public when
somebody presses Build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 20:09:29 +01:00
pastilhasandClaude Opus 5 b27969ba57 Servers is a block in Administration, not a section of its own
It is the same KIND of thing as the root location and the databases — this
machine, not the site's content — and a nav row leading to one card was a whole
screen to reach three lines.

Last of the three, because it needs both of the others: a server has nowhere to
run without the checkout and nothing to read without a database.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 13:53:39 +01:00
pastilhasandClaude Opus 5 2e56260a88 a Servers section: development and production, both under pm2
pm2 for BOTH, development included. A `bun dev` started in a terminal dies with
the terminal, and nobody can tell whether it is running without finding the
window it was started in. Under pm2 the two are the same kind of thing — a named
process with a status, an uptime and a log, startable from here and inspectable
from anywhere. The cost is that development stops printing to a console somebody
is watching, which is what `pm2 logs fofos-dev` is for; the process names are in
the panel so nobody has to guess them.

This plugin owns exactly two names, `fofos-dev` and `fofos-prod`, and every
command is built from that pair and from nothing a caller supplies. No request
can stop, restart or delete another process on the machine. That matters more
than usual here: Officer's own processes share this pm2 daemon, and a plugin
able to name any of them would be a way to take the host down through a web
form.

Start is really a replace — delete, then start. Not `restart`: it fails on a
process that has never existed, and on one that has it reuses the environment it
was started with, so a port or database changed here would be saved, displayed,
and not in effect. That is an hour of somebody's debugging.

The environment is assembled here because this plugin is what knows the ports
and the database names, and it is handed to pm2 through the invoking command's
own environment — pm2 has no flag for it; `--env` names a block in an ecosystem
file. The site reads .env files first and the environment last, so this fills
what is absent rather than overriding what somebody wrote down.

Ports have no default. It is the one setting where a plausible guess is worst:
it binds, the site appears to work, and it is quietly answering on a port
another service wanted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 13:43:48 +01:00
pastilhasandClaude Opus 5 3dd4dd6592 one header instead of three rows, and the folder lives in the URL
THE TOP WAS THREE ROWS OF FURNITURE before any content: the workspace's chrome
header saying "Detail", the panel's own strip saying "Media — Photos and video the
site is built from", and then the browser's toolbar.

a panel meta may supply its own `header`, which is the mechanism for exactly this and
which gitea, invoices and desktop already use. so the section's name goes where a
panel's name goes, and the strip inside is gone. "Detail" was never information —
the panel is the detail panel, and the useful word is which section is open.

the hint went with it. "Photos and video the site is built from" earns its place in
the NAV, where somebody is choosing between ten sections. repeated above the one they
chose, it is a caption for a decision already made.

AND THE FOLDER IS IN THE QUERY STRING. `urlPath` puts it in `?path=`, so a reload
lands where you were and a link can be sent to somebody — which for a media folder
somebody else uploads into is the point. it is opt-in in the platform because a
screen with two browsers would have them fighting over one parameter; this screen has
one.

`panelId` stays alongside it: the URL covers a reload or a shared link, and panelId
covers a panel re-opened later, which the URL cannot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 08:56:15 +01:00
pastilhasandClaude Opus 5 c38d269faf 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>
2026-08-29 08:44:11 +01:00
pastilhasandClaude Opus 5 944d3d9c5b a Media section, browsing the site's originals in place
a new section above Administration. it embeds THE PLATFORM'S OWN file browser rather
than drawing a file list — upload, delete, rename, move, thumbnails, the context
menu and every task hanging off it already exist and are maintained in one place. a
plugin's own would be a second to keep in step, arriving without any of that.

`basePath` IS A JAIL, not a starting folder: `useFileBrowserApp` resets currentPath
whenever it leaves, so pointing it at `<root>/media/originals` means the breadcrumb
stops there and `derived/` is not reachable by navigating. that is what was asked
for — people work in originals, derived is made for them.

IT IS A GUARDRAIL AND NOT A PERMISSION BOUNDARY, and the panel says so in its own
docstring because the difference will matter to whoever reads this next. the
file-browser API is unchanged and still addresses the whole home: anyone with file
access reaches derived/ through the Files screen. this prevents an accident, not a
determined person. confining it properly means scoping that API per user, which is a
platform change and a much larger one.

the panel also says, once, that derived/ exists and is made from these — otherwise
somebody who knows about it would think it had been lost.

`rootRelative` is new on the settings API because only the SERVER knows what home
is. the file browser addresses everything relative to a root, and a browser deriving
that prefix from the absolute path would be guessing. null when the root sits
outside home — a legitimate setting the browser cannot address, which the panel
explains rather than rendering an empty folder.

NOTHING HERE IS FOFOS-SPECIFIC, which matters now that this plugin is the template
for other site admins: the root is a setting, media/originals is the layout this
plugin creates, and both are read from the API. another site's plugin changes the
label.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 08:39:53 +01:00
pastilhasandClaude Opus 5 fcc7436965 Administration is emptied, and starts again with a Database block
the address-and-token form is gone from the screen and ConnectionPanel.tsx with
it. leaving an unreferenced component behind would have been worse than deleting
it: it is in the history, and a file nothing renders rots without anyone noticing.

in its place, Administration is a LIST OF BLOCKS rather than one form. it will
hold several unrelated settings, and giving each a titled block now means the
second one moves nothing — a single form would have to be broken up the first time
anything joined it.

the first block is Database, and it is empty on purpose. it says "nothing to set
yet" rather than rendering nothing, because a section that draws blank reads as
one somebody abandoned. what goes in it is not decided, and inventing fields to
fill the space would be deciding it in the wrong place.

WHAT THIS COSTS, plainly: there is now no way to configure this plugin from the
screen at all. the server half is untouched — `/_config` still stores a URL and a
token and the proxy in api/router.ts still reads them — but nothing can set them.
that is consistent with the gate being hardcoded false rather than a second
breakage: both are the same unfinished change, and neither is worth half-fixing
while the shape of the connection is being replaced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 00:28:24 +01:00
pastilhasandClaude Opus 5 10b7bc34f7 nothing but Administration opens until the plugin is ready
every section was clickable and every one of them showed the same connection form,
because the panel forced it and the nav did not know. nine doors onto one message
taught nobody anything and made a setup step look like nine faults.

so there is now a gate, `useReady`, and both panels read it. that is the point of
putting it in a hook rather than in either panel: a nav that offers a row the
detail panel then refuses is worse than one that does not offer it.

the nav DECLINES TO OFFER — a locked section is a <div>, not a <Link>, so there is
no href to hover, no cmd-click into a new tab and nothing to focus. rendering a
link and swallowing the click leaves a row that looks navigable and behaves like
one everywhere except the click.

the detail panel HOLDS THE LOCK, and it has to, because `?section=gallery` typed
into the address bar or arriving from an old bookmark never passes through the
nav.

one line of copy in the foot rather than a badge on nine dimmed rows. without it a
locked nav reads as broken instead of as one step not done: the rows go grey and
nothing says why or what to do about it.

THE CONDITION IS HARDCODED FALSE, as asked. the plumbing around it is real — it
asks /status, re-asks when the connection changes, and reports while it is still
asking — so switching the gate on is one line. it returns a single boolean and not
its parts deliberately: "all the info needed in Administration" will grow past
`configured && lastProbeOk`, and callers must not start assembling their own
version of it out of the pieces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 00:25:57 +01:00
pastilhasandClaude Opus 5 7eec2ec51d Administration is a section, not an overlay
the connection form — where fofos is, and the token — was reached through
`?config=1` and a link in the detail header, because it was not one of the nine
sections. it is now the tenth, called Administration, last in the nav.

that removes the overlay and the second route to it. the two could disagree: the
form covered whatever section was selected underneath, so the nav highlighted one
thing while the panel showed another, and an unconfigured plugin rendered the form
with no header at all — which reads as the selected section being broken rather
than as one setup step not done yet. unconfigured now goes to Administration and
the header says so.

`site` and `administration` are deliberately separate and the file says why. Site
settings is the CLIENT's — partners, social links, contact details, all of it
public copy. Administration is Officer's end: how this plugin reaches fofos, and
whatever else it will need to know. nothing in it ever appears on the site, and
the people who touch the two are not the same people.

`path: null`, alone among the sections: it reads nothing from fofos, which is why
it keeps working when fofos is unreachable and the others do not.

the header's settings link is replaced by the section's own hint, so the row still
says what it is once you are in it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 00:18:13 +01:00
PastilhasandClaude Opus 5 6b87956e44 Everything needed to be a plugin
manifest, one table, a router and two panels. Discovery is by convention, so
this is the point at which the directory stops being a directory and becomes
something Officer can install.

The manifest holds only what a listing cannot say. One permission, grantable,
with ownerOnly on /_config — reading and editing the site is exactly what a
member might be brought in for, but the token is unrestricted write authority
over everything the public sees, so handing it over is the owner's.

db/schema.ts is ONE table and should stay that way. fofos owns its own Postgres
and its own media store; this holds a URL and a token. That is what makes the
strongest thing about this plugin true: no operation on it can destroy the
client's work, checkbox ticked or not, because none of it was ever here. A cache
would be the thin end of that.

The router mostly forwards. fofos's /api/admin already answers in the shape a
panel wants, so re-wrapping it would be a second place for the contract to drift.
Two things in it are load-bearing rather than style:

  registerProxiedPrefix('/api/fofos-admin/media'). bodyParser calls parseBody on
  multipart across the whole protected router, and Hono then caches the parsed
  FormData — so asking for the bytes afterwards re-serialises them under a NEW
  boundary while the ORIGINAL content-type header is what gets forwarded. Header
  and body disagree and every upload fails with "Unexpected end of form".
  Narrow to the one route: everything else is JSON that is fine to parse.

  The catch-all is LAST in the file. Hono runs matching handlers in registration
  order, so /* above /_config would swallow it and the settings form would
  forward itself to fofos, which has no such endpoint — a 404 from a plugin that
  is working perfectly.

Panels are a nav and a detail pane that never speak to each other: the nav writes
?section=, the detail pane reads it. Real <Link>s rather than the buttons
proxy-manager uses — AGENTS.md says nav items are links, and the file documenting
that pattern says not to copy the older one. <Link> and not <NavLink> because
active state here is a query parameter and NavLink compares paths, which would
light every row at once.

Which component renders which section is a MAP, not a conditional on `built`.
The first version read `built ? <MessagesPanel/> : <Planned/>`, which is correct
only while exactly one section exists — the second one finished would have
rendered Messages under its own name.

Messages is the only section built, chosen because it is one read-only route: it
proves the path from an Officer panel through this proxy to fofos and back with
nothing else able to be wrong at the same time. The other eight are in the nav,
dimmed and labelled, saying what they will do and that the old admin still edits
them. A nav that hides what is unfinished gives no sense of what the thing is for.

Typechecked against the platform — and the typechecker was verified to actually
cover plugins/ by planting an error and watching it fail, because a clean run
that does not include your files is worse than no run.

NOT yet: installed, or run against a live fofos. Nothing here has been through
the permission gate or a real request.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 23:05:34 +01:00