From 4bfc7fae2a0c76b862877db96acc660e64cc04e3 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sun, 21 Jun 2026 13:19:18 +0100 Subject: [PATCH] Implemented about page. --- .gitignore | 3 ++ Jenkinsfile | 10 ++++++ src/server/servermanager.js | 42 +++++++++++++++++++++++++ src/socket/__tests__/socketuser.test.js | 27 ++++++++++++++++ src/socket/socketuser.js | 11 +++++++ 5 files changed, 93 insertions(+) create mode 100644 src/server/servermanager.js diff --git a/.gitignore b/.gitignore index f7f4f8f..4226d61 100644 --- a/.gitignore +++ b/.gitignore @@ -135,6 +135,9 @@ dist test-results.xml +# Jenkins generated build metadata +src/buildInfo.json + DS_STORE **/DS_Store diff --git a/Jenkinsfile b/Jenkinsfile index 95f25bf..8f3577f 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -23,6 +23,16 @@ pipeline { } } + stage('Write Build Metadata') { + steps { + nodejs(nodeJSInstallationName: 'Node23') { + sh ''' + node -e "const fs = require('fs'); fs.writeFileSync('src/buildInfo.json', JSON.stringify({ buildNumber: process.env.BUILD_NUMBER || 'dev' }, null, 2) + '\\n');" + ''' + } + } + } + stage('Install Dependencies') { steps { nodejs(nodeJSInstallationName: 'Node23') { diff --git a/src/server/servermanager.js b/src/server/servermanager.js new file mode 100644 index 0000000..a7d1fcb --- /dev/null +++ b/src/server/servermanager.js @@ -0,0 +1,42 @@ +import { readFileSync } from 'node:fs'; +import log4js from 'log4js'; +import { loadConfig } from '../config.js'; + +const config = loadConfig(); + +// Setup logger +const logger = log4js.getLogger('Server Manager'); +logger.level = config.server.logLevel; + +const readJsonFile = fileUrl => { + try { + return JSON.parse(readFileSync(fileUrl, 'utf8')); + } catch (error) { + if (error?.code !== 'ENOENT') { + logger.debug('Failed to read server metadata:', error?.message); + } + return {}; + } +}; + +const packageJsonUrl = new URL('../../package.json', import.meta.url); +const buildInfoUrl = new URL('../buildInfo.json', import.meta.url); + +/** + * ServerManager exposes websocket server metadata to authenticated socket users. + */ +export class ServerManager { + constructor(socketClient) { + this.socketClient = socketClient; + } + + getServerVersion() { + const packageJson = readJsonFile(packageJsonUrl); + const buildInfo = readJsonFile(buildInfoUrl); + + return { + version: packageJson.version ?? 'dev', + buildNumber: buildInfo.buildNumber ?? 'dev' + }; + } +} diff --git a/src/socket/__tests__/socketuser.test.js b/src/socket/__tests__/socketuser.test.js index 14429e0..69e4d0f 100644 --- a/src/socket/__tests__/socketuser.test.js +++ b/src/socket/__tests__/socketuser.test.js @@ -67,6 +67,15 @@ jest.unstable_mockModule('../../notification/notificationmanager.js', () => ({ })) })); +jest.unstable_mockModule('../../server/servermanager.js', () => ({ + ServerManager: jest.fn().mockImplementation(() => ({ + getServerVersion: jest.fn(() => ({ + version: '1.0.0', + buildNumber: 'dev' + })) + })) +})); + jest.unstable_mockModule('log4js', () => ({ default: { getLogger: () => ({ @@ -116,6 +125,10 @@ describe('SocketUser', () => { 'disconnect', expect.any(Function) ); + expect(mockSocket.on).toHaveBeenCalledWith( + 'getServerVersion', + expect.any(Function) + ); }); describe('handleAuthenticateEvent', () => { @@ -224,6 +237,20 @@ describe('SocketUser', () => { }); }); + describe('handleGetServerVersionEvent', () => { + it('should return server version details', async () => { + const callback = jest.fn(); + + await socketUser.handleGetServerVersionEvent({}, callback); + + expect(socketUser.serverManager.getServerVersion).toHaveBeenCalled(); + expect(callback).toHaveBeenCalledWith({ + version: '1.0.0', + buildNumber: 'dev' + }); + }); + }); + describe('handleDisconnect', () => { it('should remove all listeners', async () => { await socketUser.handleDisconnect(); diff --git a/src/socket/socketuser.js b/src/socket/socketuser.js index 41bd7d9..28efb8b 100644 --- a/src/socket/socketuser.js +++ b/src/socket/socketuser.js @@ -9,6 +9,7 @@ import { ActionManager } from '../actions/actionmanager.js'; import { EventManager } from '../events/eventmanager.js'; import { StatsManager } from '../stats/statsmanager.js'; import { NotificationManager } from '../notification/notificationmanager.js'; +import { ServerManager } from '../server/servermanager.js'; const config = loadConfig(); @@ -29,6 +30,7 @@ export class SocketUser { this.eventManager = new EventManager(this); this.statsManager = new StatsManager(this); this.notificationManager = new NotificationManager(this); + this.serverManager = new ServerManager(this); this.templateManager = socketManager.templateManager; this.keycloakAuth = new KeycloakAuth(); this.setupSocketEventHandlers(); @@ -84,6 +86,10 @@ export class SocketUser { 'generateHostOtp', this.handleGenerateHostOtpEvent.bind(this) ); + this.socket.on( + 'getServerVersion', + this.handleGetServerVersionEvent.bind(this) + ); this.socket.on('objectAction', this.handleObjectActionEvent.bind(this)); this.socket.on('disconnect', this.handleDisconnect.bind(this)); } @@ -245,6 +251,11 @@ export class SocketUser { callback(result); } + async handleGetServerVersionEvent(data, callback) { + const responseCallback = typeof callback === 'function' ? callback : data; + responseCallback(this.serverManager.getServerVersion()); + } + async handleObjectActionEvent(data, callback) { await this.actionManager.sendObjectAction( data._id,