From 12ec007bb84bb1dc5f8d7dbc11400fb6c95fe2ed Mon Sep 17 00:00:00 2001 From: NGPixel Date: Sat, 8 Aug 2026 03:34:06 -0400 Subject: [PATCH] feat: animate more admin icons + add puppeteer install option + various fixes --- backend/api/system.ts | 2 +- backend/models/extensions.ts | 66 +++++++++++++++---- .../extensions/puppeteer/definition.yml | 11 +++- backend/package.json | 3 +- dev/build/Dockerfile | 12 +++- .../icons/fluent-apps-tab-animated.svg | 17 +++++ .../icons/fluent-bunch-of-keys-animated.svg | 16 +++++ .../fluent-find-and-replace-animated.svg | 18 +++++ .../icons/fluent-inspection-animated.svg | 27 ++++++++ .../_assets/icons/fluent-network-animated.svg | 45 +++++++++++++ .../fluent-swiss-army-knife-animated.svg | 28 ++++++++ .../icons/fluent-windsock-animated.svg | 26 ++++++++ frontend/src/components/PageActionsCol.vue | 7 +- frontend/src/pages/AdminApprovals.vue | 2 +- frontend/src/pages/AdminDashboard.vue | 2 +- frontend/src/pages/AdminExtensions.vue | 14 +++- frontend/src/pages/AdminFlags.vue | 2 +- frontend/src/pages/AdminInstances.vue | 2 +- frontend/src/pages/AdminLogin.vue | 2 +- frontend/src/pages/AdminSearch.vue | 2 +- frontend/src/pages/AdminUtilities.vue | 2 +- 21 files changed, 276 insertions(+), 30 deletions(-) create mode 100644 frontend/public/_assets/icons/fluent-apps-tab-animated.svg create mode 100644 frontend/public/_assets/icons/fluent-bunch-of-keys-animated.svg create mode 100644 frontend/public/_assets/icons/fluent-find-and-replace-animated.svg create mode 100644 frontend/public/_assets/icons/fluent-inspection-animated.svg create mode 100644 frontend/public/_assets/icons/fluent-network-animated.svg create mode 100644 frontend/public/_assets/icons/fluent-swiss-army-knife-animated.svg create mode 100644 frontend/public/_assets/icons/fluent-windsock-animated.svg diff --git a/backend/api/system.ts b/backend/api/system.ts index 908735528..b2e0e10f2 100644 --- a/backend/api/system.ts +++ b/backend/api/system.ts @@ -509,7 +509,7 @@ async function routes(app: FastifyInstance) { schema: { summary: 'Install or reinstall an extension', description: - 'Only extensions flagged `isInstallable` can be installed from here — currently Sharp, which is an npm package. It already ships as an optional dependency, so this is mostly a repair: it refetches the package and the prebuilt binary for this OS and architecture, which is what to reach for when the native binary is missing or does not match the platform. Git and Pandoc come from the operating system and answer 409 pointing at the documentation. Runs npm and can take minutes.', + 'Only extensions flagged `isInstallable` can be installed from here — the npm packages, which are Sharp and Puppeteer. For Sharp this is mostly a repair: it already ships as an optional dependency, and refetching it replaces a prebuilt binary that is missing or does not match this OS and architecture. Puppeteer is not shipped at all, so this is a first install, and it fetches a Chromium build of a few hundred megabytes unless the server points at one it already has through `PUPPETEER_EXECUTABLE_PATH`. Git and Pandoc come from the operating system and answer 409 pointing at the documentation. Runs npm and can take minutes — allow the request a correspondingly long timeout.', tags: ['System'], params: { type: 'object', diff --git a/backend/models/extensions.ts b/backend/models/extensions.ts index 162af46c0..466241232 100644 --- a/backend/models/extensions.ts +++ b/backend/models/extensions.ts @@ -7,8 +7,14 @@ import { promisify } from 'node:util' const execFileAsync = promisify(execFile) -/** How long an install may run before it is given up on — fetching a native binary is not instant. */ -const installTimeout = 5 * 60 * 1000 +/** + * How long an install may run before it is given up on. + * + * Generous because of what the slowest one has to do: Puppeteer fetches a Chromium build of a few + * hundred megabytes, which on a thin connection is minutes of transfer before npm has anything to + * unpack. A ceiling rather than a wait — Sharp still finishes in seconds. + */ +const installTimeout = 20 * 60 * 1000 /** How much npm output is kept when reporting a failure, taken from the end where the error is. */ const installErrorLength = 800 @@ -33,6 +39,15 @@ export interface ExtensionDefinition { platforms?: string[] /** Whether the admin area can install it, as opposed to it being installed by hand. */ isInstallable: boolean + /** + * The version `install()` asks npm for. + * + * For an extension that is not declared in `package.json` at all, which is the only place a version + * would otherwise be written down — without it npm resolves whatever is newest today, and two + * instances installed a month apart are running different software. An extension the manifest + * already declares leaves this out, since a second pin here could only disagree with the first. + */ + installVersion?: string } /** An extension plus its state on this system, as exposed by the API. */ @@ -98,10 +113,11 @@ async function moduleExists(specifier: string): Promise { * Puppeteer. Each lives in `modules/extensions//definition.yml`, which declares how to detect it, * what it is compatible with, and whether it can be installed from here. * - * Most cannot: a Git or Pandoc binary comes from the system package manager, and the admin area links - * out to the instructions. An extension detected as a `module` is an npm package, and `install()` can - * (re)install that with npm — which for Sharp, shipped as an optional dependency, is how a native - * binary that is missing or does not match the platform gets replaced. + * The `command` ones cannot be installed from here: a Git or Pandoc binary comes from the system + * package manager, and the admin area links out to the instructions instead. An extension detected as + * a `module` is an npm package, which `install()` can fetch — Sharp to replace a native binary that + * is missing or does not match the platform, Puppeteer because it is deliberately not shipped and has + * to come from somewhere. */ class Extensions { /** Definitions read from disk, refreshed by `refreshFromDisk()`. */ @@ -201,16 +217,34 @@ class Extensions { * `isInstallable` and `isCompatible` first; this repeats the detection check afterwards, since npm * exiting zero and the module actually being there are not the same claim. * - * Reinstalling is the point as much as installing is. Sharp is a declared optional dependency, so an - * ordinary install already has it — what goes wrong is its *native* binary: an image built on one - * platform and run on another, or an install that skipped optional dependencies, leaves the JavaScript - * package in place and the binary for this OS and architecture missing. Hence the flags: + * The two installable extensions ask for different things, and the flags below serve both. + * + * Sharp is a declared optional dependency, so an ordinary install already has it — reinstalling is + * the point as much as installing is. What goes wrong is its *native* binary: an image built on one + * platform and run on another, or an install that skipped optional dependencies, leaves the + * JavaScript package in place and the binary for this OS and architecture missing. + * + * Puppeteer is not declared anywhere, so this is a genuine first install, and the bulk of it is the + * browser. Nothing has to be arranged for that: Puppeteer's own postinstall fetches one into its + * cache, which is the ordinary case and the one an install straight onto Linux takes. A server that + * already has a browser opts out with `PUPPETEER_SKIP_DOWNLOAD` and points at it with + * `PUPPETEER_EXECUTABLE_PATH` — what the Docker image does with the Chromium it takes from the + * distro. Neither is required, and neither is set here: npm inherits this process's environment, so + * an install from the admin area sees exactly what the operator set for the server and nothing else. + * + * Hence the flags: * * - `--no-save` because the manifest already declares the package, and an HTTP request has no * business rewriting the manifests the release was built from. * - `--force` so npm refetches rather than deciding an already-present but unusable copy is fine. * - `--include=optional` because the per-platform binaries are themselves optional dependencies of * the package, and omitting them is the usual cause of the failure being repaired here. + * - `--no-ignore-scripts` because the browser IS Puppeteer's postinstall. An operator who has set + * `ignore-scripts` — a reasonable thing to harden an npm config with — would otherwise get the + * package with no browser under it, npm exiting zero, and this model reporting it as installed: + * the failure would surface much later, as a render that cannot start a browser. Which scripts + * are trusted is still decided by the `allowScripts` policy in `package.json`, and a package + * denied there is skipped whatever this flag says. * * @throws If the extension cannot be installed this way, if npm fails, or if the module is still * missing afterwards @@ -220,8 +254,13 @@ class Extensions { throw new Error(`${definition.title} is not an npm package, so it cannot be installed here.`) } const specifier = definition.detect.value + // -> What npm is asked for, which carries the pin; what is checked for afterwards is the package + // name on its own, since that is what lands in `node_modules` + const request = definition.installVersion + ? `${specifier}@${definition.installVersion}` + : specifier - WIKI.logger.info(`Installing extension ${definition.key} (npm package ${specifier})...`) + WIKI.logger.info(`Installing extension ${definition.key} (npm package ${request})...`) try { const { stdout } = await execFileAsync( process.platform === 'win32' ? 'npm.cmd' : 'npm', @@ -230,9 +269,10 @@ class Extensions { '--no-save', '--force', '--include=optional', + '--no-ignore-scripts', '--no-audit', '--no-fund', - specifier + request ], { cwd: WIKI.SERVERPATH, @@ -250,7 +290,7 @@ class Extensions { WIKI.logger.warn(`Failed to install extension ${definition.key}:`) WIKI.logger.warn(detail || err) throw new Error( - `npm could not install ${specifier}: ${detail.slice(-installErrorLength) || 'no output'}` + `npm could not install ${request}: ${detail.slice(-installErrorLength) || 'no output'}` ) } diff --git a/backend/modules/extensions/puppeteer/definition.yml b/backend/modules/extensions/puppeteer/definition.yml index 49171f64d..e012a2c47 100644 --- a/backend/modules/extensions/puppeteer/definition.yml +++ b/backend/modules/extensions/puppeteer/definition.yml @@ -2,7 +2,8 @@ key: puppeteer title: Puppeteer description: >- Headless Chromium browser. Required to export pages as PDF and to render content elements on the - server, such as Mermaid or PlantUML diagrams. + server, such as Mermaid or PlantUML diagrams. Installing it downloads a Chromium build of a few + hundred megabytes, unless the server already provides one through PUPPETEER_EXECUTABLE_PATH. website: 'https://pptr.dev' detect: type: module @@ -10,4 +11,10 @@ detect: architectures: - x64 - arm64 -isInstallable: false +# Not a declared dependency of the backend, unlike Sharp: a browser is worth a few hundred megabytes +# only to an instance that renders pages on the server, so installing it is a decision rather than a +# default. That makes this a real first install and not merely a repair. +isInstallable: true +# The version installed, here rather than in `package.json` for the reason above. The Docker image +# reads this same line, so an image and a hand-installed instance agree on what Puppeteer is. +installVersion: 25.4.0 diff --git a/backend/package.json b/backend/package.json index 7feb29a21..d6f6f1d30 100644 --- a/backend/package.json +++ b/backend/package.json @@ -112,6 +112,7 @@ "dev": true, "releaseDate": "2026-01-01T01:01:01.000Z", "allowScripts": { - "esbuild": false + "esbuild": false, + "puppeteer": true } } diff --git a/dev/build/Dockerfile b/dev/build/Dockerfile index 7d8b90fb0..e77dcf5f0 100644 --- a/dev/build/Dockerfile +++ b/dev/build/Dockerfile @@ -41,9 +41,15 @@ RUN npm ci --omit=dev # The Puppeteer extension, which server-side page rendering needs. Added here rather than declared in # `backend/package.json` because it is an optional extension: an installation that renders its pages in # the editor -- which is all of them, on any normal save -- has no use for a browser on the server, and -# a source checkout should not have to fetch one to install the backend. Pinned like every other -# dependency, so an image build is reproducible. -RUN npm install --no-save puppeteer@25.4.0 +# a source checkout should not have to fetch one to install the backend. +# +# The version is read from the extension definition, which is also what the admin area installs when an +# operator adds Puppeteer to an instance by hand: one place to bump, and an image that cannot drift from +# what a hand-installed instance gets. An empty read fails the build rather than quietly installing +# whatever is newest. +RUN PUPPETEER_VERSION="$(sed -n 's/^installVersion: *//p' modules/extensions/puppeteer/definition.yml)" && \ + test -n "$PUPPETEER_VERSION" && \ + npm install --no-save "puppeteer@${PUPPETEER_VERSION}" WORKDIR /wiki diff --git a/frontend/public/_assets/icons/fluent-apps-tab-animated.svg b/frontend/public/_assets/icons/fluent-apps-tab-animated.svg new file mode 100644 index 000000000..235e7bc28 --- /dev/null +++ b/frontend/public/_assets/icons/fluent-apps-tab-animated.svg @@ -0,0 +1,17 @@ + + + diff --git a/frontend/public/_assets/icons/fluent-bunch-of-keys-animated.svg b/frontend/public/_assets/icons/fluent-bunch-of-keys-animated.svg new file mode 100644 index 000000000..132f3f928 --- /dev/null +++ b/frontend/public/_assets/icons/fluent-bunch-of-keys-animated.svg @@ -0,0 +1,16 @@ + + + diff --git a/frontend/public/_assets/icons/fluent-find-and-replace-animated.svg b/frontend/public/_assets/icons/fluent-find-and-replace-animated.svg new file mode 100644 index 000000000..9aa9cf2ae --- /dev/null +++ b/frontend/public/_assets/icons/fluent-find-and-replace-animated.svg @@ -0,0 +1,18 @@ + + + diff --git a/frontend/public/_assets/icons/fluent-inspection-animated.svg b/frontend/public/_assets/icons/fluent-inspection-animated.svg new file mode 100644 index 000000000..ec4947f18 --- /dev/null +++ b/frontend/public/_assets/icons/fluent-inspection-animated.svg @@ -0,0 +1,27 @@ + + + + + diff --git a/frontend/public/_assets/icons/fluent-network-animated.svg b/frontend/public/_assets/icons/fluent-network-animated.svg new file mode 100644 index 000000000..c85e96da9 --- /dev/null +++ b/frontend/public/_assets/icons/fluent-network-animated.svg @@ -0,0 +1,45 @@ + + + + + + + + + + diff --git a/frontend/public/_assets/icons/fluent-swiss-army-knife-animated.svg b/frontend/public/_assets/icons/fluent-swiss-army-knife-animated.svg new file mode 100644 index 000000000..4da29451e --- /dev/null +++ b/frontend/public/_assets/icons/fluent-swiss-army-knife-animated.svg @@ -0,0 +1,28 @@ + + + diff --git a/frontend/public/_assets/icons/fluent-windsock-animated.svg b/frontend/public/_assets/icons/fluent-windsock-animated.svg new file mode 100644 index 000000000..01d8686a8 --- /dev/null +++ b/frontend/public/_assets/icons/fluent-windsock-animated.svg @@ -0,0 +1,26 @@ + + + diff --git a/frontend/src/components/PageActionsCol.vue b/frontend/src/components/PageActionsCol.vue index c411b3518..b3fcf1b20 100644 --- a/frontend/src/components/PageActionsCol.vue +++ b/frontend/src/components/PageActionsCol.vue @@ -132,7 +132,10 @@ auto-close transition-show="jump-left"> - + @@ -144,7 +147,7 @@ Rerender Page - + diff --git a/frontend/src/pages/AdminApprovals.vue b/frontend/src/pages/AdminApprovals.vue index 2cad57bd2..d8f084bfb 100644 --- a/frontend/src/pages/AdminApprovals.vue +++ b/frontend/src/pages/AdminApprovals.vue @@ -2,7 +2,7 @@
- +
{{ t('admin.approval.title') }}
diff --git a/frontend/src/pages/AdminDashboard.vue b/frontend/src/pages/AdminDashboard.vue index 2e7e7bc12..a8cfb4b32 100644 --- a/frontend/src/pages/AdminDashboard.vue +++ b/frontend/src/pages/AdminDashboard.vue @@ -2,7 +2,7 @@
- +
{{ t('admin.dashboard.title') }}
diff --git a/frontend/src/pages/AdminExtensions.vue b/frontend/src/pages/AdminExtensions.vue index a407ef0c1..726237a17 100644 --- a/frontend/src/pages/AdminExtensions.vue +++ b/frontend/src/pages/AdminExtensions.vue @@ -143,6 +143,16 @@ const state = reactive({ extensions: [] }) +/** + * How long to give an install, in milliseconds. + * + * Stated because the client's own default is ten seconds, which no npm install finishes inside: the + * request would be abandoned here while npm carried on running on the server, reporting a failure for + * something that was about to succeed and leaving the administrator to install it twice. Matches the + * ceiling the server puts on the same work, Puppeteer's browser download being what sets it. + */ +const INSTALL_TIMEOUT = 20 * 60 * 1000 + // METHODS async function load() { @@ -167,7 +177,9 @@ async function install(ext) { html: true }) try { - const resp = await API_CLIENT.post(`system/extensions/${ext.key}/install`).json() + const resp = await API_CLIENT.post(`system/extensions/${ext.key}/install`, { + timeout: INSTALL_TIMEOUT + }).json() if (!resp?.ok) { throw new Error(resp?.message || 'An unexpected error occured') } diff --git a/frontend/src/pages/AdminFlags.vue b/frontend/src/pages/AdminFlags.vue index 309ec6fa1..d2a0a05d5 100644 --- a/frontend/src/pages/AdminFlags.vue +++ b/frontend/src/pages/AdminFlags.vue @@ -2,7 +2,7 @@
- +
{{ t('admin.flags.title') }}
diff --git a/frontend/src/pages/AdminInstances.vue b/frontend/src/pages/AdminInstances.vue index d65662446..f2c9f14d3 100644 --- a/frontend/src/pages/AdminInstances.vue +++ b/frontend/src/pages/AdminInstances.vue @@ -2,7 +2,7 @@
- +
{{ t('admin.instances.title') }}
diff --git a/frontend/src/pages/AdminLogin.vue b/frontend/src/pages/AdminLogin.vue index 6ad4209ae..551c808b4 100644 --- a/frontend/src/pages/AdminLogin.vue +++ b/frontend/src/pages/AdminLogin.vue @@ -2,7 +2,7 @@