You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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
+
59
60
-`GET /admin/` — the page (server-side rendered from a live snapshot, so
60
61
`curl` shows real content; an inline script re-polls every ~10s — when
61
62
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
88
89
89
90
The headline: **how much of a wp-admin/Nextcloud-style admin can the plugin
90
91
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:
93
96
94
97
| wp-admin pillar | what this plugin does today | the seam that would close the gap |
95
98
|---|---|---|
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|
97
100
|**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 |
98
101
|**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 |
99
102
|**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 |
100
103
|**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.)|
102
105
|**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 |
103
106
|**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?" |
104
107
@@ -111,14 +114,14 @@ Beyond the table:
111
114
host clearly has one (whoever writes `createServer`'s config). An
112
115
`api.isOperator(agent)` (or an `operators: [...]` server option plugins
113
116
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.
122
125
3.**A guarded plugin page has no session story.** Core's UI has its own
123
126
login; a plugin page can't ride it — the only credential a plugin can
124
127
check is what arrives on the request, so the browser poller has to shuttle
@@ -137,18 +140,21 @@ Beyond the table:
137
140
5.**Composition worked.** This plugin is `dashboard/`'s probes +
138
141
`metrics/`'s healthz idea + `nip05/`'s podsRoot scan + `shortlink/`'s
139
142
`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.
142
147
143
148
## Test
144
149
145
150
```
146
151
node --test --test-concurrency=1 admin/test.js
147
152
```
148
153
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
152
158
`adminAgents` set to a pod minted via `/.pods` + `/idp/credentials`:
153
159
anonymous → 401, second pod's bearer → 403, admin bearer → the full page
154
160
(plugin ids, server-side-rendered pods count, the not-possible strip) and
0 commit comments