plugins: a backend plugin can have settings

A plugin with no web/ has no route, no dock tile and no panel — so it had
nowhere to put a setting. transcribe shipped exactly like that: installed,
running, and configurable only by an HTTP call somebody had to already know
about.

The plugin DECLARES its fields and the platform renders them, on the plugin
detail page — the one screen that exists for every plugin whether or not it has
a frontend. A component would have survived settings too complicated to
describe, at the cost of every backend plugin carrying a frontend to draw one
text field, each looking slightly different.

The platform stores nothing. It is a form over the plugin's own /config: GET to
populate, PUT to save. So a plugin that validates before saving still does, and
one that keeps a credential keeps it where it already kept it.

A `secret` field is write-only across the boundary — never read back, rendered
as a password, and left out of the PUT when untouched so an untyped box is not
read as "clear it". The plugin reports whether it HAS one, never the value.

Eight strings across all six locales, because the key type refuses anything
else — which is the localisation work from earlier tonight doing its job.

1426 pass, tsgo clean, docs:check green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-24 04:00:04 +00:00
co-authored by Claude Opus 5
parent d74e07ef08
commit c427cb5b61
11 changed files with 277 additions and 7 deletions
+3
View File
@@ -147,6 +147,9 @@ pluginsRouter.get('/', async (ctx) => {
version: plugin.manifest.version,
platform: plugin.manifest.platform,
permissions: plugin.manifest.permissions,
// What the owner must be able to set. Declared rather than rendered by the plugin, so a backend
// plugin with no frontend of its own still gets a settings form — see `PluginSettings.tsx`.
settings: plugin.manifest.settings ?? [],
// What the tree declared. The UI shows these so "installed but does nothing" is legible.
has: {
api: !!plugin.api,