feat: admin terminal - show connected instance

scarlett
NGPixel 4 weeks ago
parent e393e720fd
commit 95587ae71b
No known key found for this signature in database

@ -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 ]`
)
})
}
)

@ -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.",

@ -12,7 +12,14 @@
{{ t('admin.terminal.subtitle') }}
</div>
</div>
<div class="flex-none flex">
<div class="flex-none flex items-center">
<div v-if="state.connected" class="mr-4 text-right leading-tight">
<div class="text-xs text-grey">{{ t('admin.terminal.instance') }}</div>
<div class="flex items-center justify-end gap-1.5 font-mono text-sm">
<status-light class="admin-terminal-dot" color="positive" pulse />
{{ state.instance }}
</div>
</div>
<w-btn
class="acrylic-btn mr-2"
v-if="!state.connected || state.connecting"
@ -89,7 +96,9 @@ useMeta({
const state = reactive({
displayMode: 'logs',
connected: false,
connecting: false
connecting: false,
/** Which instance is on the other end of the socket, from its handshake frame. */
instance: null
})
let socket = null
@ -120,6 +129,7 @@ function connect() {
// -> Whether the stream ever started is what tells a session that was refused or never reached the
// server apart from one that ran and ended, and only `close` is guaranteed to fire
let opened = false
let handshake = false
socket.addEventListener('open', () => {
opened = true
@ -129,6 +139,15 @@ function connect() {
})
socket.addEventListener('message', (ev) => {
/*
The server's first frame is the handshake and says which instance answered; everything after it
is a log line to be printed verbatim. See `controllers/terminal.ts`.
*/
if (!handshake) {
handshake = true
state.instance = JSON.parse(ev.data).instance
return
}
term.writeln(ev.data)
})
@ -136,6 +155,7 @@ function connect() {
socket = null
state.connected = false
state.connecting = false
state.instance = null
/*
Codes in the 4000 range are the server's own (see `controllers/terminal.ts`) and mean the
session was refused rather than dropped, so the reason is worth printing reconnecting with the
@ -194,6 +214,14 @@ onBeforeUnmount(() => {
<style lang="scss">
.admin-terminal {
/* -> `status-light` is a bar sized by whatever it sits in; here it wants to be a dot */
&-dot {
width: 6px;
height: 6px;
min-height: 6px;
flex: none;
}
&-term {
width: 100%;
/* -> The terminal fits itself to this box, so the box has to have a height of its own: sized off

Loading…
Cancel
Save