plugins contribute dock tiles, and the doc says what is actually built

offscale had no dock tile: that field comes from the app store's catalogue, so a
plugin installed through the plugin system was reachable only by typing its url
or following the link on /plugins.

built at runtime rather than baked into the generated bundle, deliberately —
WHO sees a tile is a permission question, and a grant takes effect on the next
request rather than the next build. presentation comes from the manifest and the
route from mountPrefix, so there is one source for both, and  is the
plugin's first permission so the endpoint can filter a tile out for an account
that cannot reach the screen.

two sources for tiles today, because the app store still has its own catalogue.
one when it is rebuilt on this.

the doc now records the system as complete rather than half-stubbed, including
the three bugs the extraction found — the sidecar-before-mount ordering, the
missing tailwind, and the build that could delete its own shell — and what is
genuinely still open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 00:31:54 +00:00
co-authored by Claude Opus 5
parent 8587ae20b7
commit 8b6cb34ae0
3 changed files with 90 additions and 37 deletions
+45 -36
View File
@@ -605,48 +605,57 @@ Used for both `/api/...` and the frontend route. Nothing else in the codebase ma
---
## What is built, as of 2026-08-14
## What is built — complete, as of 2026-08-15
The plugin system works end to end for a plugin that has an `api/router.ts`. Verified against a running
server, with **no restart at any point**:
**Offscale is a plugin, and nothing in the system is a stub.** Validated by the owner against the live
server across repeated install / enable / disable / uninstall cycles, checking PM2 and the frontend each
time.
```
/api/example/ping BEFORE install 404
AFTER install 200 {"plugin":"example","ok":true}
AFTER disable 404
AFTER enable 200
AFTER uninstall 404
core routes throughout 200
```
| Piece | Where |
| --------------------------------------- | ---------------------------------------------------- |
| Manifest, `mountPrefix`, validation | `servers/plugins/manifest.ts` |
| Discovery by convention | `servers/plugins/discover.ts` |
| Disk ⋈ database, mounts, dock manifests | `servers/plugins/mount.ts` |
| Install runner, four verbs, streamed | `servers/plugins/install.ts` |
| PM2 ecosystem entry | `servers/plugins/ecosystem.ts` |
| Schema barrel + `db:push` | `servers/plugins/schema.ts` |
| `Plugins.gen.tsx` + `Bun.build` | `servers/plugins/generate.ts` |
| `buildHonoApp` / `rebuildHonoApp` | `servers/hono.ts` |
| Capability registration | `capabilities/registry.ts``setPluginCapabilities` |
| Install state | `plugin_installs` |
| The screen | `/plugins`, two panels, SSE log |
| The reference plugin | `plugins/example/` |
| **The first real plugin** | `plugins/offscale/` — 45 files |
| Piece | Where |
| ------------------------------------------- | --------------------------------------------- |
| Manifest type, `mountPrefix`, validation | `servers/plugins/manifest.ts` |
| Discovery by convention | `servers/plugins/discover.ts` |
| Disk ⋈ database, and the rebuild | `servers/plugins/mount.ts` |
| `buildHonoApp` / `rebuildHonoApp` | `servers/hono.ts` |
| The closure that makes the swap take effect | `server.tsx`, the `/api/*` route |
| Install state | `plugin_installs` (`officer_db/src/plugins/`) |
| The four verbs | `servers/api/plugins/router.ts`, owner-only |
| The screen | `/plugins` — two panels, `?selected=` |
| The reference plugin | `plugins/example/` — meant to be read |
Nothing needs a restart. Routes swap by rebuilding the Hono app, the sidecar gets a PM2 entry, the
frontend is regenerated and rebuilt in ~3s, capabilities are registered before routes mount, and the
whole thing survives a restart because boot regenerates and mounts before `serve()`.
### Not wired yet
### Three bugs the extraction found
Worth recording because none were visible from reading:
1. **Install started the sidecar before mounting.** `createSidecarProxy` learns its port from a one-shot
`<name>:server` event and subscribes when the plugin's router is first imported — at mount. So the
announcement fired into a void: process online, routes mounted, every request `503 sidecar not
available`. It would have hit every plugin with an HTTP sidecar; `example` never caught it because it
has no listener to announce. Install and enable now mount first.
2. **The built SPA had no Tailwind.** `bunfig.toml` declares the plugin under `[serve.static]`, which
applies to Bun's static serving and not to a programmatic `Bun.build()`.
3. **The build could destroy itself.** Clearing `build/` before building meant a failed build left
nothing, and two overlapping builds could delete each other's shell. It now stages and swaps.
### Still open
- **The schema push.** A plugin with `db/schema.ts` installs, but its tables are not created. Marked
`[open]` in the router.
- **The sidecar's PM2 entry.** A plugin with `sidecar/` installs, but no process starts. This is the same
hole `app-store/pm2.ts:23-29` documents for the app store, and it is the next thing to build.
- **Websocket providers.** `server.reload({ routes })` is proven but not called; Bun's route table is
still the six hardcoded providers.
- **Totality across plugin routes.** `PROTECTED_API_PREFIXES` remains the core list, so plugin mounts are
not covered by the boot check — and the check is reading the wrong list anyway (see below). The
assertion wants moving into `buildHonoApp`, which is now the single place routes are mounted.
### Deliberately not done
**Offscale is not extracted.** The infrastructure is ready for it, but moving it means deleting working
code across ~50 files, and that should happen with someone watching rather than unattended.
still the hardcoded providers. No plugin owns a socket yet.
- **Totality across plugin routes.** `PROTECTED_API_PREFIXES` is still the core list, and the check reads
`Object.keys(handlers)` while Bun serves the route table. The assertion wants moving into
`buildHonoApp`, which is now the single place routes are mounted.
- **Two dock sources.** The app store keeps its own catalogue, so tiles come from there and from the
plugin system. One when the app store is rebuilt on this.
- **Members.** Offscale is `ownerOnly` — read/write for members needs its queries resolving to the
OWNER's rows rather than the caller's, which is a change inside the plugin.
---