From e393e720fdd6fcc5d376b2c082b267453036cd19 Mon Sep 17 00:00:00 2001 From: NGPixel Date: Wed, 12 Aug 2026 21:55:38 -0400 Subject: [PATCH] fix: admin terminal + missing icons --- backend/controllers/terminal.ts | 66 +++++++++++ backend/core/logger.ts | 17 +++ backend/index.ts | 9 +- frontend/package-lock.json | 95 ++-------------- frontend/package.json | 2 +- .../public/_assets/icons/fluent-abc-block.svg | 1 + frontend/public/_assets/icons/fluent-bug.svg | 1 + .../public/_assets/icons/fluent-copybook.svg | 1 + .../_assets/icons/fluent-data-center.svg | 1 + .../icons/fluent-document-in-folder.svg | 1 + .../public/_assets/icons/fluent-heart.svg | 1 + frontend/public/_assets/icons/fluent-idea.svg | 1 + .../_assets/icons/fluent-pencil-drawing.svg | 2 +- .../public/_assets/icons/fluent-server.svg | 1 + .../icons/fluent-software-installer.svg | 1 + .../public/_assets/icons/fluent-software.svg | 1 + .../icons/fluent-topic-push-notification.svg | 1 + .../_assets/icons/fluent-translation.svg | 1 + .../_assets/icons/fluent-user-update.svg | 1 + .../public/_assets/icons/fluent-users.svg | 1 + frontend/src/components/HeaderActionsMenu.vue | 20 +++- frontend/src/pages/AdminTerminal.vue | 106 ++++++++++++------ frontend/vite.config.js | 35 +++--- 23 files changed, 223 insertions(+), 143 deletions(-) create mode 100644 backend/controllers/terminal.ts create mode 100644 frontend/public/_assets/icons/fluent-abc-block.svg create mode 100644 frontend/public/_assets/icons/fluent-bug.svg create mode 100644 frontend/public/_assets/icons/fluent-copybook.svg create mode 100644 frontend/public/_assets/icons/fluent-data-center.svg create mode 100644 frontend/public/_assets/icons/fluent-document-in-folder.svg create mode 100644 frontend/public/_assets/icons/fluent-heart.svg create mode 100644 frontend/public/_assets/icons/fluent-idea.svg create mode 100644 frontend/public/_assets/icons/fluent-server.svg create mode 100644 frontend/public/_assets/icons/fluent-software-installer.svg create mode 100644 frontend/public/_assets/icons/fluent-software.svg create mode 100644 frontend/public/_assets/icons/fluent-topic-push-notification.svg create mode 100644 frontend/public/_assets/icons/fluent-translation.svg create mode 100644 frontend/public/_assets/icons/fluent-user-update.svg create mode 100644 frontend/public/_assets/icons/fluent-users.svg diff --git a/backend/controllers/terminal.ts b/backend/controllers/terminal.ts new file mode 100644 index 000000000..5dd43c841 --- /dev/null +++ b/backend/controllers/terminal.ts @@ -0,0 +1,66 @@ +import type { FastifyInstance, FastifyRequest } from 'fastify' +import type { WebSocket } from 'ws' + +/** + * _terminal Routes + * + * The websocket behind the admin area's terminal view, which streams this instance's log lines to a + * browser as they are written. Read-only: nothing a client sends is looked at, and the socket carries + * exactly what `core/logger.ts` hands to `console.log`, formatted string and all — including the ANSI + * colours, which xterm renders. + * + * Only this instance's own main thread is on that stream. Worker threads build their own logger + * (`worker.ts`), and other instances write to their own consoles, so a clustered deployment shows the + * terminal of whichever instance the socket happened to land on. + * + * Log lines quote paths, e-mail addresses and query failures, so the handshake takes `manage:system` + * — the same permission as the rest of the system views, and never granted to a group by accident. + */ + +/** + * How much unsent traffic a client may accumulate before its stream starts skipping lines. + * + * A browser that has stopped reading — a backgrounded tab on a slow link, most likely — would + * otherwise have the server hold every line since it stalled. Dropping is right here: the terminal is + * a live view of what is happening now, not a transcript that has to be complete. + */ +const MAX_BUFFERED = 1048576 // 1mb + +async function routes(app: FastifyInstance) { + app.get( + '/logs', + { websocket: true, schema: { hide: true } }, + (socket: WebSocket, req: FastifyRequest) => { + /* + Refusals close the socket with a code in the private 4000 range, where the browser hands both + code and reason to the page — which is how the terminal can print why it was turned away and + know not to offer a reconnect. See `pages/AdminTerminal.vue`. + */ + if (!req.session?.authenticated) { + return socket.close(4401, 'Authentication is required') + } + if (!req.session.permissions?.includes('manage:system')) { + return socket.close(4403, 'You are not allowed to read the server logs') + } + + const send = (line: string) => { + if (socket.readyState !== socket.OPEN || socket.bufferedAmount > MAX_BUFFERED) { + return + } + socket.send(line) + } + + // -> A terminal that opens onto an idle server would otherwise sit empty and look broken + for (const line of WIKI.logger.backlog()) { + send(line) + } + + WIKI.logger.ws.on('log', send) + socket.on('close', () => { + WIKI.logger.ws.off('log', send) + }) + } + ) +} + +export default routes diff --git a/backend/core/logger.ts b/backend/core/logger.ts index a35252084..bd2b294fb 100644 --- a/backend/core/logger.ts +++ b/backend/core/logger.ts @@ -5,6 +5,12 @@ export type LogLevel = 'error' | 'warn' | 'info' | 'debug' export type IgnoredLogLevel = 'verbose' | 'silly' export type LogFn = (...args: unknown[]) => void +/** + * Formatted lines kept in memory, replayed to an admin terminal the moment it connects + * (`controllers/terminal.ts`). Enough to see how the instance got to where it is, not a log file. + */ +const BACKLOG_SIZE = 100 + const LEVELS: LogLevel[] = ['error', 'warn', 'info', 'debug'] const LEVELSIGNORED: IgnoredLogLevel[] = ['verbose', 'silly'] const LEVELCOLORS: Record = { @@ -18,6 +24,7 @@ class Logger extends EventEmitter { // -> Assigned dynamically in init(). `declare` keeps these type-only so that no class field is // emitted, leaving the runtime shape of the instance untouched. declare ws: EventEmitter + declare backlog: () => string[] declare error: LogFn declare warn: LogFn declare info: LogFn @@ -32,8 +39,13 @@ export default { const primaryLogger = new Logger() let ignoreNextLevels = false + const backlog: string[] = [] primaryLogger.ws = new EventEmitter() + // -> One listener per connected admin terminal, so the default cap of 10 is a leak warning rather + // than a limit worth respecting + primaryLogger.ws.setMaxListeners(0) + primaryLogger.backlog = () => [...backlog] LEVELS.forEach((lvl) => { primaryLogger[lvl] = (...args: unknown[]) => { @@ -58,6 +70,11 @@ export default { } console.log(formatted) + + backlog.push(formatted) + if (backlog.length > BACKLOG_SIZE) { + backlog.shift() + } primaryLogger.ws.emit('log', formatted) }) } diff --git a/backend/index.ts b/backend/index.ts index 44de19320..fe3dd02c3 100644 --- a/backend/index.ts +++ b/backend/index.ts @@ -63,6 +63,7 @@ const SERVER_ROUTE_SEGMENTS = new Set([ '_icons', '_render', '_site', + '_terminal', '_thumb', '_user' ]) @@ -273,9 +274,10 @@ async function initHTTPServer() { app.register(fastifySensible) app.register(fastifyCompress, { global: true }) /* - Websocket upgrades, for live collaborative editing (`controllers/collab.ts`). Registered on the - root instance because the upgrade handler is installed on the HTTP server itself, and before the - routes below because a route declaring `websocket: true` needs it already there. + Websocket upgrades, for live collaborative editing (`controllers/collab.ts`) and the admin + terminal's log stream (`controllers/terminal.ts`). Registered on the root instance because the + upgrade handler is installed on the HTTP server itself, and before the routes below because a + route declaring `websocket: true` needs it already there. `maxPayload` bounds a single frame: these carry keystrokes and cursor positions, and the largest legitimate one is a client handing over a document it edited while offline. @@ -666,6 +668,7 @@ async function initHTTPServer() { app.register(import('./controllers/site.ts'), { prefix: '/_site' }) app.register(import('./controllers/icons.ts'), { prefix: '/_icons' }) app.register(import('./controllers/render.ts'), { prefix: '/_render' }) + app.register(import('./controllers/terminal.ts'), { prefix: '/_terminal' }) app.register(import('./controllers/thumb.ts'), { prefix: '/_thumb' }) app.register(import('./controllers/user.ts'), { prefix: '/_user' }) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ab409044b..66112e779 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,6 +11,7 @@ "@simplewebauthn/browser": "13.3.0", "@tailwindcss/vite": "4.3.3", "@twemoji/api": "17.0.2", + "@xterm/addon-fit": "0.11.0", "@xterm/xterm": "6.0.0", "browser-fs-access": "0.38.0", "clipboard": "2.0.11", @@ -40,7 +41,6 @@ "pinia": "4.0.2", "semver": "7.8.5", "slugify": "1.6.9", - "socket.io-client": "4.8.3", "sortablejs": "1.15.7", "sortablejs-vue3": "1.3.0", "tailwindcss": "4.3.3", @@ -2067,12 +2067,6 @@ "integrity": "sha512-BE/UWv6FOToAdVk0EokzkqQQDOWtNydYlY6+OrmiZ5SCNmb41VehttboTetUM3T/fr6EAFYVXjz4My2wg230rQ==", "license": "MIT" }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", - "license": "MIT" - }, "node_modules/@tailwindcss/node": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", @@ -2650,6 +2644,12 @@ "integrity": "sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==", "license": "MIT" }, + "node_modules/@xterm/addon-fit": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz", + "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==", + "license": "MIT" + }, "node_modules/@xterm/xterm": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", @@ -2869,6 +2869,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2956,28 +2957,6 @@ "dev": true, "license": "ISC" }, - "node_modules/engine.io-client": { - "version": "6.6.4", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz", - "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.4.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.18.3", - "xmlhttprequest-ssl": "~2.1.1" - } - }, - "node_modules/engine.io-parser": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", - "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/enhanced-resolve": { "version": "5.24.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.3.tgz", @@ -3949,6 +3928,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/muggle-string": { @@ -4416,34 +4396,6 @@ "node": ">=8.0.0" } }, - "node_modules/socket.io-client": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", - "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.4.1", - "engine.io-client": "~6.6.1", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-parser": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", - "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.4.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/sortablejs": { "version": "1.15.7", "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.7.tgz", @@ -5391,27 +5343,6 @@ "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "license": "MIT" }, - "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/wsl-utils": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz", @@ -5429,14 +5360,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/xmlhttprequest-ssl": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", - "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/y-monaco": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/y-monaco/-/y-monaco-0.1.6.tgz", diff --git a/frontend/package.json b/frontend/package.json index 2f49eba3d..849899d5c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,6 +20,7 @@ "@simplewebauthn/browser": "13.3.0", "@tailwindcss/vite": "4.3.3", "@twemoji/api": "17.0.2", + "@xterm/addon-fit": "0.11.0", "@xterm/xterm": "6.0.0", "browser-fs-access": "0.38.0", "clipboard": "2.0.11", @@ -49,7 +50,6 @@ "pinia": "4.0.2", "semver": "7.8.5", "slugify": "1.6.9", - "socket.io-client": "4.8.3", "sortablejs": "1.15.7", "sortablejs-vue3": "1.3.0", "tailwindcss": "4.3.3", diff --git a/frontend/public/_assets/icons/fluent-abc-block.svg b/frontend/public/_assets/icons/fluent-abc-block.svg new file mode 100644 index 000000000..2e22bf440 --- /dev/null +++ b/frontend/public/_assets/icons/fluent-abc-block.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/_assets/icons/fluent-bug.svg b/frontend/public/_assets/icons/fluent-bug.svg new file mode 100644 index 000000000..c56a3b233 --- /dev/null +++ b/frontend/public/_assets/icons/fluent-bug.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/_assets/icons/fluent-copybook.svg b/frontend/public/_assets/icons/fluent-copybook.svg new file mode 100644 index 000000000..066219674 --- /dev/null +++ b/frontend/public/_assets/icons/fluent-copybook.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/_assets/icons/fluent-data-center.svg b/frontend/public/_assets/icons/fluent-data-center.svg new file mode 100644 index 000000000..01fd87cfd --- /dev/null +++ b/frontend/public/_assets/icons/fluent-data-center.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/_assets/icons/fluent-document-in-folder.svg b/frontend/public/_assets/icons/fluent-document-in-folder.svg new file mode 100644 index 000000000..762350780 --- /dev/null +++ b/frontend/public/_assets/icons/fluent-document-in-folder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/_assets/icons/fluent-heart.svg b/frontend/public/_assets/icons/fluent-heart.svg new file mode 100644 index 000000000..96a071ead --- /dev/null +++ b/frontend/public/_assets/icons/fluent-heart.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/_assets/icons/fluent-idea.svg b/frontend/public/_assets/icons/fluent-idea.svg new file mode 100644 index 000000000..18937afa6 --- /dev/null +++ b/frontend/public/_assets/icons/fluent-idea.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/_assets/icons/fluent-pencil-drawing.svg b/frontend/public/_assets/icons/fluent-pencil-drawing.svg index fdb425c7e..1cce2fb4b 100644 --- a/frontend/public/_assets/icons/fluent-pencil-drawing.svg +++ b/frontend/public/_assets/icons/fluent-pencil-drawing.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/frontend/public/_assets/icons/fluent-server.svg b/frontend/public/_assets/icons/fluent-server.svg new file mode 100644 index 000000000..f4f8d2de8 --- /dev/null +++ b/frontend/public/_assets/icons/fluent-server.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/_assets/icons/fluent-software-installer.svg b/frontend/public/_assets/icons/fluent-software-installer.svg new file mode 100644 index 000000000..33bed25d4 --- /dev/null +++ b/frontend/public/_assets/icons/fluent-software-installer.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/_assets/icons/fluent-software.svg b/frontend/public/_assets/icons/fluent-software.svg new file mode 100644 index 000000000..9b2c9deb6 --- /dev/null +++ b/frontend/public/_assets/icons/fluent-software.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/_assets/icons/fluent-topic-push-notification.svg b/frontend/public/_assets/icons/fluent-topic-push-notification.svg new file mode 100644 index 000000000..b991fb297 --- /dev/null +++ b/frontend/public/_assets/icons/fluent-topic-push-notification.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/_assets/icons/fluent-translation.svg b/frontend/public/_assets/icons/fluent-translation.svg new file mode 100644 index 000000000..baed10cfb --- /dev/null +++ b/frontend/public/_assets/icons/fluent-translation.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/_assets/icons/fluent-user-update.svg b/frontend/public/_assets/icons/fluent-user-update.svg new file mode 100644 index 000000000..3a6ffa59e --- /dev/null +++ b/frontend/public/_assets/icons/fluent-user-update.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/_assets/icons/fluent-users.svg b/frontend/public/_assets/icons/fluent-users.svg new file mode 100644 index 000000000..ed9c082ff --- /dev/null +++ b/frontend/public/_assets/icons/fluent-users.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/HeaderActionsMenu.vue b/frontend/src/components/HeaderActionsMenu.vue index b48405784..43e8f4f99 100644 --- a/frontend/src/components/HeaderActionsMenu.vue +++ b/frontend/src/components/HeaderActionsMenu.vue @@ -78,7 +78,10 @@ {{ t('common.header.admin') }} - + +