Commit Graph
5 Commits
Author SHA1 Message Date
pastilhasandClaude Opus 5 898209725a a plugin owns the routing under its mount
Reverts the manifest route table. Declaring a plugin's URLs to the platform put
the platform in the middle of a decision that is not its own, and it was more
code than what it replaced.

One splat where there were two routes. A splat matches zero segments, so the
bare route still works, and the plugin reads its own position off the location.
The platform owns where a plugin starts and nothing after it — the same line
mountPrefix already draws around its API.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 07:17:33 +01:00
pastilhasandClaude Opus 5 1fe767ea77 a plugin screen can declare its routes
Every screen got `/<route>` and `/<route>/:section` and nothing deeper, so a
section had to live in the query string. A screen may now name its routes in
the manifest, and give one its own arrangement of the screen's panels.

Absent, the old pair is unchanged — which is every plugin but fofos-admin.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 04:57:18 +01:00
pastilhas 6b5c87da10 a plugin can be more than one screen
a plugin was one screen because the two things it declares — a mount prefix and
a frontend — happened to be the same string. caldav is where that stops holding:
one sidecar, one permission, one install, and two screens that have nothing to
do with each other on screen. calendar and contacts are not sections of one
page, and forcing them into one would be a worse product than the core screens
they replace.

nothing else moved. all eighteen plugins with a frontend use flat
web/panels.ts, which is now "one screen, at the mount prefix, labelled by the
manifest" — the same file, the same route, the same persisted layout key. the
nested form is opt-in and caldav is its only user.

the split of who declares what follows the rule this file already had. the
DIRECTORY declares that a screen exists (web/<segment>/panels.ts); the MANIFEST
says what to call it and which glyph to draw, because a directory listing cannot
say either and those are a human's choice. the two are cross-checked and a
mismatch is refused rather than reconciled: a segment with no directory renders
nothing, a directory with no entry has no tile and no label, and both are the
quiet kind of broken.

the part that needed the most care is that a segment is a URL the plugin CHOSE
— /calendar, not /caldav/calendar. mountPrefix is what makes it impossible for a
plugin to shadow a core route, and a chosen segment steps straight out of it. so
this is first-party only, refused at discovery, and tested: without that check
this field is a way for any installed plugin to take over /files.

screenId, not appName, is now the layout key and the panel allow-list's owner.
two screens sharing one would persist over each other's sizes and each would
accept the other's panels. existing plugins get screenId === appName, so no
stored layout is orphaned — verified by regenerating.
2026-08-27 04:15:51 +00:00
pastilhasandClaude Opus 5 a7b61a07eb the structure is the author's, the sizes are the user's
a locked screen persisted its whole layout tree, and `locked` means the user
cannot add, move or close a panel — so the only thing in that blob they could
actually change was how big things are. everything else was a copy of the
author's design sitting in the user's slot, where it could only go stale or get
damaged. it did both.

STALE: ship a new defaultLayout and nobody who had opened the screen ever saw
it. their copy won forever, with no way back a person could reach.

DAMAGED: WorkspaceView's appTypes guard replaces an unrecognised appType with
the fallback and writes the repair back — right for a user-authored dashboard,
catastrophic here. today a plugin layout naming a panel that did not exist yet
was rewritten to two copies of its first panel and SAVED. it survived every
reload and every fix, and the row had to be deleted from postgres. twice,
because an open tab wrote its in-memory copy back over the first delete.

useAuthoredLayout takes the structure from source on every render and persists
only sizes. a stale structure cannot exist, a repair has nothing to write
itself into, and a layout change reaches everybody on the next load — no
version to bump, no migration, nothing for an author to remember. the
corruption stops being repaired and becomes unrepresentable.

a group whose child count no longer matches keeps the author's numbers: two
sizes cannot describe three panels, and applying them anyway would leave the
new one with the remainder. that guard is what makes shipping a layout change
safe rather than merely possible.

what is stored is source-with-your-sizes, a valid layout because the route and
a CHECK constraint both demand one — written and never read as structure. the
asymmetry is the design.

PluginScreen only, for now. every core screen is locked and has the same bug;
that is a mechanical sweep of one call each and wants its own commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
2026-08-25 17:47:18 +00:00
pastilhasandClaude Opus 5 543e88a9a6 every plugin route renders a workspace, and it is not a rule you can forget
an exclusionary rule, made structural. a plugin does not render a screen: it
contributes panels and says how they are arranged, and the shell renders
WorkspaceView around them.

    web/panels.ts   appRegistryMetas — at least one panel
    web/layout.ts   defaultLayout — how they are arranged

both required the moment web/ exists, and missing either is refused at discovery
by name and with the reason. tested:

    probeplug: has a web/ directory but is missing web/layout.ts.
    Every plugin route renders a Workspace: contribute panels and a layout,
    not a screen.

there is deliberately no way to export a component. one that could would be free
to render a bare div, a full-page form, or its own navigation, and the platform
would become a shell hosting strangers' layouts rather than one application.
non-compliance is not so much refused as unrepresentable — there is nowhere to
put a screen.

the shell registers <prefix> and <prefix>/:section, exactly as the core screens
do, so a plugin's sections stay addressable and cmd-clickable, and panels read
useParams independently rather than passing state between themselves.
appTypes.allowed is pinned to that plugin's own keys, so a persisted layout
naming something else falls back instead of rendering another plugin's panel
inside this screen.

the example plugin is rebuilt to model it — two panels, a layout, one of them
calling its own /api/example/ping through useClient — because the reference
implementation is what everyone copies.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 23:47:48 +00:00