plugins: enable, disable, delete — and uninstall is gone
Three states all read as "stopped": disabled, uninstalled, and uninstalled-with-the-directory-still- there. Nothing on screen distinguished them, and the one that mattered was invisible — an uninstalled plugin kept its directory, which silently decided what the next install would run. That is how a jellyfin install on alpha re-applied a checkout from two days earlier while reporting success at every step. So uninstall stops being a verb. What it did survives as the first half of deleting: it stops the sidecar, removes the install row and the PM2 entry, unpublishes the copies, and revokes grants naming permissions that are about to stop existing. Delete is offered only on a plugin that is not running — disable is one reversible click, so a live plugin is always two deliberate steps from gone. It opens a confirmation carrying a checkbox, "also delete all its data", unticked and never inherited from last time. Unticked, the directory goes and the rows stay, and they appear in the orphan list further down the same screen. Ticked, the tables go with it, which is the only way to a machine that has never had the plugin. The checkbox picks between two ROUTES, not a body flag on one: a flag can be added to a request by anything that already knows how to call the safe verb, and the difference here is somebody's rows. Grants are revoked either way, which is load-bearing until the permissions work tomorrow: a grant naming a key the platform no longer recognises made the whole permissions screen unsaveable, because the page submits every grant it loaded. Also corrected two rationales that the change made false — the runbook's "uninstall never destroys data" verification table, and npm-deps.ts explaining directory survival in terms of uninstall. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,6 @@ import {
|
||||
pluginProcessStatus,
|
||||
setPluginRunning,
|
||||
purgePlugin,
|
||||
uninstallPlugin,
|
||||
type PluginActionResult,
|
||||
} from '../../plugins/install';
|
||||
import { updatePlugin, syncCapabilities } from '../../plugins/update';
|
||||
@@ -218,15 +217,20 @@ pluginsRouter.post('/:appName/install', async (ctx) => {
|
||||
return ctx.json(result, result.ok ? 200 : 400);
|
||||
});
|
||||
|
||||
// Destructive, and the only route here that is. Named `purge` rather than being a flag on uninstall so
|
||||
// nothing can reach it by passing a body field to the verb people already use.
|
||||
pluginsRouter.post('/:appName/purge', async (ctx) => {
|
||||
return ctx.json(await purgePlugin(ctx.req.param('appName')));
|
||||
// ── Two doors, because "delete" means two things ──
|
||||
//
|
||||
// `delete` takes the plugin off the machine and leaves its tables. `purge` takes the data as well. Two
|
||||
// routes rather than one route and a `dropData` body field: a flag can be added to a request by anything
|
||||
// that already knows how to call the safe verb, and the difference here is somebody's rows.
|
||||
//
|
||||
// The UI's checkbox chooses which one it calls, so the confirmation the owner reads and the door that
|
||||
// opens are the same decision.
|
||||
pluginsRouter.post('/:appName/delete', async (ctx) => {
|
||||
return ctx.json(await purgePlugin(ctx.req.param('appName'), { dropData: false }));
|
||||
});
|
||||
|
||||
pluginsRouter.post('/:appName/uninstall', async (ctx) => {
|
||||
const result = await uninstallPlugin(ctx.req.param('appName'));
|
||||
return ctx.json(result, result.ok ? 200 : 400);
|
||||
pluginsRouter.post('/:appName/purge', async (ctx) => {
|
||||
return ctx.json(await purgePlugin(ctx.req.param('appName'), { dropData: true }));
|
||||
});
|
||||
|
||||
pluginsRouter.post('/:appName/enable', async (ctx) => {
|
||||
@@ -239,7 +243,7 @@ pluginsRouter.post('/:appName/disable', async (ctx) => {
|
||||
return ctx.json(result, result.ok ? 200 : 400);
|
||||
});
|
||||
|
||||
// ── The same four verbs, streamed ──
|
||||
// ── The same verbs, streamed ──
|
||||
//
|
||||
// An install writes an ecosystem entry, starts a process and rebuilds the router. Collecting all of that
|
||||
// and answering once means a spinner that stops, with no way to tell "started the sidecar" from "could
|
||||
@@ -261,10 +265,11 @@ type VerbFn = (
|
||||
|
||||
const VERBS = {
|
||||
install: (appName, onStep, userId, onLog) => installPlugin(appName, onStep, userId, onLog),
|
||||
uninstall: (appName, onStep, _userId, onLog) => uninstallPlugin(appName, onStep, onLog),
|
||||
enable: (appName, onStep) => setPluginRunning(appName, true, onStep),
|
||||
disable: (appName, onStep) => setPluginRunning(appName, false, onStep),
|
||||
purge: (appName, onStep, _userId, onLog) => purgePlugin(appName, onStep, onLog),
|
||||
// Same function, and the verb IS the flag — see the two routes above.
|
||||
delete: (appName, onStep, _userId, onLog) => purgePlugin(appName, { dropData: false }, onStep, onLog),
|
||||
purge: (appName, onStep, _userId, onLog) => purgePlugin(appName, { dropData: true }, onStep, onLog),
|
||||
// A pull and then the ordinary install path — see plugins/update.ts. It is a verb rather than its own
|
||||
// route so it inherits the streaming door and the UI that already drives install.
|
||||
update: (appName, onStep, userId, onLog) => updatePlugin(appName, onStep, userId, onLog),
|
||||
|
||||
Reference in New Issue
Block a user