etherpad-lite/admin
John McLear 2a865a3a4a
feat(admin): show "requires newer Etherpad" when installing incompatible plugin (#7763) (#7771)
* feat(admin): show "requires newer Etherpad" when installing incompatible plugin (#7763)

Old admins on out-of-date Etherpad installations get no feedback when they
click Install on a plugin that needs a newer core. live-plugin-manager
doesn't honor engines.node, and the admin UI dropped the error payload
that adminplugins.ts already emits.

This wires up an end-to-end signal:

- pluginEngineCheck.ts: pure helper comparing a plugin's engines.node
  range against process.version, with a stable error code
  (PLUGIN_REQUIRES_NEWER_ETHERPAD) and a message that avoids leaking
  the Node-version implementation detail. Unparseable ranges fall
  through as compatible so the preflight is opportunistic, not a
  gate. 8 unit tests cover the happy + edge paths.

- installer.ts: install() now best-effort fetches the published
  plugin's engines.node from npmjs.org, runs the preflight, and
  short-circuits with the typed error before invoking
  live-plugin-manager. Also wraps the body in try/catch so the
  error reaches the socket callback (it was silently dropped on
  every install failure today, including network errors).

- HomePage.tsx: surfaces finished:install.error as a toast, using a
  dedicated i18n key when the code is PLUGIN_REQUIRES_NEWER_ETHERPAD
  and a generic fallback otherwise.

- en.json: two new strings, parameterized by {{plugin}} and
  {{error}}.

The message admins see is intentionally about Etherpad, not Node —
upgrading Etherpad pulls the Node requirement along with it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(plugins): don't restart server when every install in the batch failed

Qodo PR review on #7771 caught a side effect introduced earlier in this PR.

Before this PR, install() never invoked its callback on error, so a failed
install left the task counter inflated and onAllTasksFinished() never ran —
masking, but not fixing, the bug. Once install() correctly propagates errors
to its cb, the counter hits zero on failure too, and onAllTasksFinished()
fires hooks.aCallAll('restartServer'). A no-op preflight rejection
(EngineIncompatibleError) would then disconnect every connected pad.

Fix: extract wrapTaskCb + task state into InstallerTaskQueue and track
whether at least one task in the current batch succeeded. Only fire the
"all finished" side effect when something actually changed. Failed-only
batches do nothing.

Seven unit tests cover the matrix: single success, single failure (the
regression), mixed batch (still restarts), all-failed batch, batch
reset, null cb, two-task drain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(plugins): time-bound the engines preflight registry fetch

Qodo PR review on #7771 flagged that fetchPluginEnginesNode awaits
fetch() with no timeout. A stalled DNS lookup or hung connection to
registry.npmjs.org would block install() forever — the finished:install
socket event would never fire and the admin UI would stay spinning with
no error to surface.

Wrap the fetch in AbortSignal.timeout(5000). On any failure (network,
HTTP error, abort) fetchPluginEnginesNode returns undefined, which the
preflight then treats as "no engines info → compatible," so a slow
registry never blocks an install that would otherwise succeed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 13:09:17 +01:00
..
public feat(gdpr): admin UI for author erasure (follow-up to #7550) (#7667) 2026-05-14 08:11:48 +01:00
scripts feat(admin): document admin endpoints in OpenAPI (#7693) (#7705) 2026-05-10 15:55:33 +08:00
src feat(admin): show "requires newer Etherpad" when installing incompatible plugin (#7763) (#7771) 2026-05-16 13:09:17 +01:00
.eslintrc.cjs Feat/admin react (#6211) 2024-03-09 23:07:09 +01:00
.gitignore Feat/admin react (#6211) 2024-03-09 23:07:09 +01:00
index.html Feat/admin react (#6211) 2024-03-09 23:07:09 +01:00
package.json build(deps-dev): bump the dev-dependencies group with 3 updates (#7759) 2026-05-15 19:24:07 +01:00
README.md feat(admin): document admin endpoints in OpenAPI (#7693) (#7705) 2026-05-10 15:55:33 +08:00
tsconfig.json feat(admin): document admin endpoints in OpenAPI (#7693) (#7705) 2026-05-10 15:55:33 +08:00
tsconfig.node.json Feat/admin react (#6211) 2024-03-09 23:07:09 +01:00
vite.config.ts admin: parsed JSONC settings editor (takes over #7666, closes #7603) (#7709) 2026-05-15 10:41:37 +01:00

Admin UI

Vite + React 19 single-page app served at /admin. Talks to the backend over socket.io for the existing settings / plugins / pads pages, and (when endpoints are added to the OpenAPI spec) over a typed REST client.

Scripts

Script What it does
pnpm dev gen:api + Vite dev server (expects backend on :9001).
pnpm gen:api Regenerates src/api/{schema.d.ts,version.ts} from the OpenAPI spec.
pnpm build gen:api + tsc + vite build.
pnpm build-copy Same, but writes into ../src/templates/admin.
pnpm test gen:api + smoke tests for the API client wiring.
pnpm lint ESLint.

Typed API client

The admin uses openapi-typescript to generate types from src/node/hooks/express/openapi.ts, openapi-fetch for typed requests, and openapi-react-query for TanStack Query bindings.

Generated files

admin/src/api/schema.d.ts and admin/src/api/version.ts are generated by gen:api and gitignored — never commit them. They are produced by:

pnpm --filter admin gen:api

admin/scripts/gen-api.mjs loads src/node/hooks/express/openapi.ts, calls generateDefinitionForVersion for the latest API version, pipes the JSON through openapi-typescript to produce schema.d.ts, and emits a runtime constant LATEST_API_VERSION (read from info.version in the spec) to version.ts so client.ts can build the right /api/<version>/ baseUrl.

gen:api runs as the first step of dev, build, build-copy, and test, so a fresh checkout produces the generated files automatically when any of those scripts is invoked. After modifying any of the following, the next pnpm <dev|build|test> will refresh the generated files; you can also run gen:api directly:

  • src/node/hooks/express/openapi.ts
  • src/node/handler/APIHandler.ts (changes to latestApiVersion)
  • the resource definitions referenced by openapi.ts

Using the client

import { $api } from './api/client';

const SettingsPanel = () => {
  const { data } = $api.useQuery('get', '/admin/settings'); // example
  return <pre>{JSON.stringify(data, null, 2)}</pre>;
};

The admin endpoints are not yet present in the OpenAPI spec — this client is in place to support upcoming work (see issue #7638 follow-up). For now, it is exercised only by the smoke test.