Skip to content

Commit 84234fc

Browse files
admin: consume api.plugins + api.serverInfo (phase-2 step 1)
Retrofit the operator console onto the two shipped seams, same as dashboard/: - Auto-discovers every sibling via api.plugins (#610) — the hand-fed config.plugins inventory is gone, drift impossible. config.probes now only refines individual rows ({ probe?, expect?, kind?, description?, adminPage? } by id); admin filters ITSELF out of the roster by prefix. - Reaches the host via api.serverInfo() (#601) — no config.loopbackUrl. Origin (loopback + display baseUrl) resolved per request. Result: admin needs NO required config, so it's loadable from the CLI like dashboard — boots and serves (open, no pod stats). Verified live on published 0.0.219. Still re-declared: podsRoot (the api mediates no data-root access) and the operator gate (adminAgents — no api.isOperator yet); both documented, both graceful when absent. NOT_POSSIBLE swaps the now-shipped api.plugins row for the 'gated by default' (api.isOperator) gap. serve.js drops the shared INVENTORY array (both consoles auto-discover now); compose.test.js + serve.js admin configs updated. Full suite 385/385.
1 parent 60db34d commit 84234fc

5 files changed

Lines changed: 194 additions & 223 deletions

File tree

admin/README.md

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -34,28 +34,29 @@ const fastify = createServer({
3434
config: { loopbackUrl: `http://127.0.0.1:${PORT}` } },
3535

3636
{ id: 'admin', module: 'plugins/admin/plugin.js', prefix: '/admin',
37-
config: {
38-
loopbackUrl: `http://127.0.0.1:${PORT}`, // required (no api.serverInfo)
39-
baseUrl: PUBLIC_URL, // optional, shown on the page
40-
podsRoot: PODS, // optional → pod/storage stats
37+
config: { // all optional
38+
podsRoot: PODS, // → pod/storage stats
4139
// Who is an operator? The api has no answer, so this plugin invents
4240
// one: an agent-id allowlist. Omit it and the page serves OPEN
4341
// (loud warn at activate).
4442
adminAgents: ['https://pod.example/alice/profile/card.jsonld#me'],
45-
// A HAND-COPIED duplicate of the list above — no api.plugins
46-
// registry to read (#463/#464); the two lists drift silently.
47-
plugins: [
48-
{ id: 'rss', prefix: '/feed', description: 'Atom/RSS feeds',
49-
probe: '/feed/atom', expect: [400] },
50-
{ id: 'metrics', prefix: '/metrics', description: 'healthz + Prometheus',
51-
probe: '/metrics/healthz', expect: [200], adminPage: '/metrics/metrics' },
52-
{ id: 'admin', prefix: '/admin', description: 'this page' },
53-
],
43+
// The plugin list is auto-discovered via api.plugins (#610) — no
44+
// hand-copied inventory. `probes` only *refines* individual rows
45+
// (the roster carries no health hints):
46+
probes: {
47+
rss: { probe: '/feed/atom', expect: [400], description: 'Atom/RSS feeds' },
48+
metrics: { probe: '/metrics/healthz', expect: [200], description: 'healthz + Prometheus', adminPage: '/metrics/metrics' },
49+
},
5450
} },
5551
],
5652
});
5753
```
5854

55+
Or from the CLI — since admin needs no required config, it loads via
56+
`--plugin` (open, no pod stats): `jss start --plugin ./admin/plugin.js@/admin`
57+
alongside other plugins. Add `podsRoot`/`adminAgents` via a config file to
58+
enable stats and gate it.
59+
5960
- `GET /admin/` — the page (server-side rendered from a live snapshot, so
6061
`curl` shows real content; an inline script re-polls every ~10s — when
6162
guarded, append `#token=<bearer>` to the URL so the poller can authenticate).
@@ -88,17 +89,19 @@ with its seam. **Meta**: "3 declared — 2 up, 0 degraded, 0 down, 1 unprobed
8889

8990
The headline: **how much of a wp-admin/Nextcloud-style admin can the plugin
9091
api express today?** Answer: most of the *read* side, none of the *write*
91-
side — and every read pillar leans on operator-repeated config. Pillar by
92-
pillar:
92+
side. Two read pillars that used to lean on operator-repeated config now run
93+
on **shipped seams** — the inventory on `api.plugins` (#610) and the origin on
94+
`api.serverInfo` (#601) — so what's still re-declared is `podsRoot` and the
95+
operator gate. Pillar by pillar:
9396

9497
| wp-admin pillar | what this plugin does today | the seam that would close the gap |
9598
|---|---|---|
96-
| **Plugin inventory** | renders a hand-copied `config.plugins` list; drifts silently from the real `createServer` list (add a plugin and forget the admin entry → it never appears; remove one → it keeps probing) | `api.plugins → [{ id, prefix, module }]` — the #463/#464 app-registry; the loader already holds exactly this data |
99+
| **Plugin inventory** | ✅ auto-discovered via `api.plugins` (#610) — no hand-copied list, no drift | closed. The roster is `{ id, prefix, module }` with no health hints, so ws endpoints / odd prefixes still want a `config.probes` refinement; an optional per-entry `health`/`probe` hint would close that residual too |
97100
| **Install / marketplace** | nothing — architecturally impossible; shown in the "not possible yet" strip | a package source + runtime install (#200 marketplace); would also need the loader to activate entries after boot |
98101
| **Enable / disable** | nothing — the loader activates at boot only; `deactivate()` exists in the plugin contract but nothing calls it at runtime | runtime `activate()`/`deactivate()` on the loader, driven by an authorized surface |
99102
| **Settings panels** | links to a per-plugin `adminPage` path the operator declares — each plugin hand-rolls its own page and this one merely points at it | a contribute-a-panel affordance (a plugin registers a settings fragment/route with the admin surface); today there is no gated hook or registration point for it |
100103
| **Health / status** | ✅ the strongest pillar: host liveness + per-plugin anonymous loopback probes, live per request, `expect`/`ws` semantics from `dashboard/` | mostly closed already; per-plugin *self-reported* health (`health: () => …` on a registry entry) would beat anonymous probing |
101-
| **Users / storage** | ✅ if the operator repeats `podsRoot`: pod count, du-style recursive size (capped at 50k entries, truncation reported, realpath'd root, symlinks never followed, contents never read), newest pod mtime | the api doesn't mediate any filesystem access — `podsRoot` re-declares what the host already knows (cousin of `api.serverInfo`); an `api.storage.stats()` or accounts enumeration would do this honestly |
104+
| **Users / storage** | ✅ if the operator repeats `podsRoot`: pod count, du-style recursive size (capped at 50k entries, truncation reported, realpath'd root, symlinks never followed, contents never read), newest pod mtime | the api doesn't mediate any filesystem access — `podsRoot` re-declares what the host already knows; an `api.storage.stats()` / data-root accessor would do this honestly. (The origin repetition this used to share is gone — `api.serverInfo` shipped.) |
102105
| **Log viewer** | nothing — `api.log` is write-only | an `api.log` tailing/reading seam (ring buffer or stream); until then "recent logs" can't exist on any plugin page |
103106
| **Auth / roles** | invents an operator concept from scratch: `config.adminAgents`, an agent-id allowlist checked via `api.auth.getAgent`; without it the surface is OPEN behind a warn | an operator/role concept in the api. Today every ops plugin invents its own admin auth — `metrics/` a shared token, `terminal/` a token, this one an agent allowlist — three plugins, three incompatible answers to "who is an operator?" |
104107

@@ -111,14 +114,14 @@ Beyond the table:
111114
host clearly has one (whoever writes `createServer`'s config). An
112115
`api.isOperator(agent)` (or an `operators: [...]` server option plugins
113116
can read) would let every ops surface in this repo share one answer.
114-
2. **The pods stats are a config-repetition, not an api capability.** The
115-
plugin can count users and measure storage *only because* the operator
116-
passed the same `root` path to both `createServer` and this plugin's
117-
config — the api itself mediates no storage introspection. Same shape as
118-
`api.serverInfo` (the origin repeated in ~17 configs) and the same shape
119-
as the inventory (the plugins list repeated): the whole read side of this
120-
admin page is the operator telling the plugin what the host already knows,
121-
three different ways.
117+
2. **Pod stats are the last config-repetition standing.** The plugin can
118+
count users and measure storage *only because* the operator passed the same
119+
`root` path to both `createServer` and this plugin's config — the api
120+
mediates no storage introspection. This used to be one of *three* things
121+
the operator re-declared (origin, plugin list, data root); the first two
122+
shipped (`api.serverInfo`, `api.plugins`) and admin now consumes them, so
123+
the read side is down to this one. An `api.storage.stats()` / data-root
124+
accessor would close it.
122125
3. **A guarded plugin page has no session story.** Core's UI has its own
123126
login; a plugin page can't ride it — the only credential a plugin can
124127
check is what arrives on the request, so the browser poller has to shuttle
@@ -137,18 +140,21 @@ Beyond the table:
137140
5. **Composition worked.** This plugin is `dashboard/`'s probes +
138141
`metrics/`'s healthz idea + `nip05/`'s podsRoot scan + `shortlink/`'s
139142
`getAgent` guard in one page, with no new primitive needed — evidence
140-
that the api's *read-side* vocabulary is genuinely composable. What it
141-
could not compose away is any of the four repeated-config findings above.
143+
that the api's *read-side* vocabulary is genuinely composable. Two of the
144+
repeated-config findings it once carried are now closed (it consumes
145+
`api.plugins` and `api.serverInfo`); `podsRoot` and the operator gate
146+
remain.
142147

143148
## Test
144149

145150
```
146151
node --test --test-concurrency=1 admin/test.js
147152
```
148153

149-
Validation throws first (missing `loopbackUrl`, external probe URL,
150-
self-probe recursion — before the long-lived boot; the `DATA_ROOT` footgun),
151-
then one JSS with admin + rss + metrics, `podsRoot` at the server root and
154+
Validation throws first (external probe URL, self-probe recursion — before
155+
the long-lived boot; the `DATA_ROOT` footgun), then one JSS with admin + rss
156+
+ metrics auto-discovered (no hand-fed list, no `loopbackUrl`), `podsRoot` at
157+
the server root and
152158
`adminAgents` set to a pod minted via `/.pods` + `/idp/credentials`:
153159
anonymous → 401, second pod's bearer → 403, admin bearer → the full page
154160
(plugin ids, server-side-rendered pods count, the not-possible strip) and

0 commit comments

Comments
 (0)