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:
@@ -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,
|
||||
|
||||
@@ -182,6 +182,22 @@ export type OsDependency = {
|
||||
};
|
||||
};
|
||||
|
||||
/** One field on a plugin's settings form. See `settings` on the manifest for why this is declared. */
|
||||
export type PluginSetting = {
|
||||
/** The key in the JSON body sent to `<prefix>/config`. */
|
||||
key: string;
|
||||
/** What the owner sees beside the input. */
|
||||
label: string;
|
||||
/** One line under it — what this is for, or where to find the value. Optional. */
|
||||
hint?: string;
|
||||
/** `url` and `text` render as text inputs; `secret` is write-only and renders as a password. */
|
||||
type: 'url' | 'text' | 'secret';
|
||||
/** Refuse to save without it. */
|
||||
required?: boolean;
|
||||
/** Shown greyed in an empty input, never submitted. */
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
export type PluginManifest = {
|
||||
/**
|
||||
* Who published it. The ONLY input to `mountPrefix`, so first-party and third-party can never become two
|
||||
@@ -346,6 +362,33 @@ export type PluginManifest = {
|
||||
* The host is always loopback and cannot be set. A plugin naming an arbitrary host would be a
|
||||
* request-forgery primitive reachable by anyone the plugin is granted to.
|
||||
*/
|
||||
/**
|
||||
* Settings the owner must be able to set, rendered by the platform on the plugin's own detail page.
|
||||
*
|
||||
* ── Why a declaration and not a component ──
|
||||
*
|
||||
* A plugin with no `web/` has nowhere to put a settings screen — no route, no dock tile, no panel. That
|
||||
* is most backend plugins, and it left them configurable only by an HTTP call somebody had to know
|
||||
* about. `transcribe` shipped in exactly that state: installed, running, and with no way to tell it
|
||||
* where its server was.
|
||||
*
|
||||
* The plugin could ship a component instead, and that survives settings too complicated to describe.
|
||||
* But every backend plugin would then carry a frontend to render one text field, and they would each
|
||||
* look slightly different. A declaration means the platform renders one consistent form and the plugin
|
||||
* ships no UI at all — which for a URL and a token is the whole requirement.
|
||||
*
|
||||
* ── The contract ──
|
||||
*
|
||||
* The platform GETs and PUTs `<mountPrefix>/config`, and the plugin owns what that means. Values are
|
||||
* never stored by the platform: it is a form over the plugin's own route, so a plugin that keeps a
|
||||
* credential keeps it, and a plugin that validates before saving still does.
|
||||
*
|
||||
* A field marked `secret` is write-only across the boundary — sent when set, never read back, and
|
||||
* rendered as a password input. The plugin returns whether it HAS one, never the value, the same shape
|
||||
* the Jellyfin access token already uses.
|
||||
*/
|
||||
settings?: PluginSetting[];
|
||||
|
||||
websocket?: { sidecar: string };
|
||||
osDependencies?: OsDependency[];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user