Files
fofos-admin/manifest.ts
T
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

120 lines
5.2 KiB
TypeScript

import type { PluginManifest } from '@@/plugins/manifest';
/**
* Fofos — the fofos website's admin, inside Officer.
*
* ── What it replaces ──
*
* A server-rendered admin on its own subdomain with its own argon2 passwords, its own session cookie
* and its own `/admin/setup` bootstrap. Every part of that duplicated something Officer already does,
* and none of it could be shared: giving somebody access to the machine still meant giving them
* access to the admin, separately, by hand. That is the whole reason this is a plugin.
*
* ── It holds no content ──
*
* fofos owns its own database and its own media store, and this reads and writes them over HTTP. The
* only thing in Officer's database is the connection — a URL and a token.
*
* That is a deliberate property rather than a convenience. **Deleting this plugin cannot lose a
* photograph or a line of the client's copy**, with the "delete its data" checkbox ticked or not,
* because none of it was ever here. The alternative — a `db/schema.ts` carrying fofos's own tables —
* would have put every one of them under an installer that is allowed to drop them.
*
* ── The permission is grantable; the credential is not ──
*
* Reading and editing the site is exactly what a member might be brought in to do. The TOKEN is a
* different matter: it is unrestricted write authority over everything the public sees, so `/_config`
* is `ownerOnly` — the split that field exists for, and the same shape proxy-manager and Jellyfin use
* for the same reason.
*
* ── No `screens`, but a route table ──
*
* One screen at the mount prefix, labelled `label`: the sections are panels of one workspace, and
* `screens` is for a plugin contributing genuinely separate destinations with their own dock tiles.
*
* They ARE routes, though. `routes` below names each one, which is what moved the sections out of the
* query string and into the path — and what lets a route arrange the screen's panels its own way when
* one of them needs to.
*/
export const manifest: PluginManifest = {
publisher: 'officerdev',
version: '2026.08.29.26',
platform: '>=1.0.0',
label: 'Fofos',
summary: "The fofos website's content — pages, galleries, drops and collabs",
/** one website's admin, and useless without that website. */
tags: ['Personal'],
icon: 'PenLine',
color: '#c2410c',
// Nothing to declare. It talks HTTP to fofos and stores one row; there is no host binary anywhere
// on the path, and fofos itself is a separate deployment rather than something this installs.
/*
The rich text editor for the site's copy.
Declared here rather than added to the platform's package.json, which is frozen and shared: these
are installed into `plugins/node_modules/` alongside every other plugin's, so nothing about this
plugin's editor can move a version the platform depends on.
`@tiptap/extension-text-align` was already in the platform's dependencies with nothing importing
it — a leftover from an earlier attempt. It is asked for here too, so this plugin's copy is
resolved by the same rule as the rest rather than by whatever happens to be up the tree.
*/
npmDependencies: {
'@tiptap/react': '^3.15.3',
'@tiptap/starter-kit': '^3.15.3',
'@tiptap/pm': '^3.15.3',
'@tiptap/extension-text-align': '^3.15.3',
'@tiptap/extension-link': '^3.15.3',
},
/*
The URLs this admin has.
── Why they are declared rather than guessed ──
Every plugin screen gets `/<route>` and `/<route>/:section` for free, which is enough for one
segment. These need two — `/fofos-admin/layout/header`, `/fofos-admin/pages/home` — and naming them
is what makes each one a place the platform knows about rather than a shape it inferred.
── They all share one layout, for now ──
No `layout` on any entry, so each renders `web/layout.ts`. That is the point of declaring them
anyway: the day the page builder wants a third column and Layout does not, it is one field here
rather than a new mechanism.
── The pairs ──
`layout` and `layout/:band` are two entries rather than `layout/:band?`, so either can take its own
arrangement later. React-router ranks a static segment above a dynamic one, so `/pages/home` cannot
be swallowed by something broader.
*/
routes: [
// The bare screen. Redirected to Layout by the detail panel — see DEFAULT_SECTION.
{ path: '' },
{ path: 'layout' },
{ path: 'layout/:band' },
{ path: 'pages' },
{ path: 'pages/:slug' },
{ path: 'galleries' },
{ path: 'media' },
{ path: 'administration' },
],
permissions: [
{
key: 'fofos-admin',
label: 'Fofos',
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
// BODY. A member who could call it would have an oracle pointed at a credential that can
// rewrite the whole public site.
ownerOnly: ['/_config'],
},
],
};