diff --git a/backend/controllers/terminal.ts b/backend/controllers/terminal.ts index 5dd43c841..353c6de09 100644 --- a/backend/controllers/terminal.ts +++ b/backend/controllers/terminal.ts @@ -5,9 +5,9 @@ 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. + * browser as they are written. Read-only: nothing a client sends is looked at, and every frame after + * the first is exactly what `core/logger.ts` hands to `console.log`, formatted string and all — + * including the ANSI colours, which xterm renders. The first frame is the handshake below. * * 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 @@ -43,6 +43,15 @@ async function routes(app: FastifyInstance) { return socket.close(4403, 'You are not allowed to read the server logs') } + const user = req.session.user?.email ?? req.session.user?.id ?? 'unknown' + + /* + Logged before the listener is attached, so the line is already in the backlog by the time it + is replayed below and the terminal opens on its own arrival. Every other connected terminal + sees it live, which is the point: who is reading the logs is itself worth logging. + */ + WIKI.logger.info(`Streaming server logs to user ${user}... [ CONNECTED ]`) + const send = (line: string) => { if (socket.readyState !== socket.OPEN || socket.bufferedAmount > MAX_BUFFERED) { return @@ -50,6 +59,14 @@ async function routes(app: FastifyInstance) { socket.send(line) } + /* + The handshake, and the only frame that is not a log line: which instance the client ended up + talking to, since in a cluster that is the one thing the logs themselves cannot tell it — a + line names the instance that WROTE it, and the backlog replayed below was written by this one. + Sent before anything else, so "the first frame" is all the client has to know to find it. + */ + socket.send(JSON.stringify({ instance: WIKI.INSTANCE_ID })) + // -> A terminal that opens onto an idle server would otherwise sit empty and look broken for (const line of WIKI.logger.backlog()) { send(line) @@ -57,7 +74,12 @@ async function routes(app: FastifyInstance) { WIKI.logger.ws.on('log', send) socket.on('close', () => { + // -> Off the stream first, so this instance's own goodbye is not sent down a socket that is + // already closing WIKI.logger.ws.off('log', send) + WIKI.logger.info( + `User ${user} has disconnected from server logs streaming. [ DISCONNECTED ]` + ) }) } ) diff --git a/backend/locales/en.json b/backend/locales/en.json index cb00cee7a..71637eef5 100644 --- a/backend/locales/en.json +++ b/backend/locales/en.json @@ -1026,8 +1026,9 @@ "admin.terminal.connecting": "Connecting to server...", "admin.terminal.disconnect": "Disconnect", "admin.terminal.disconnected": "Disconnected.", + "admin.terminal.instance": "Connected to instance", "admin.terminal.logs": "Logs", - "admin.terminal.subtitle": "View process logs in real-time", + "admin.terminal.subtitle": "View server logs in real-time", "admin.terminal.title": "Terminal", "admin.theme.accentColor": "Accent Color", "admin.theme.accentColorHint": "The accent color for elements that need to stand out or grab the user attention.", diff --git a/frontend/src/pages/AdminTerminal.vue b/frontend/src/pages/AdminTerminal.vue index d627a745f..2bd532b33 100644 --- a/frontend/src/pages/AdminTerminal.vue +++ b/frontend/src/pages/AdminTerminal.vue @@ -12,7 +12,14 @@ {{ t('admin.terminal.subtitle') }} -