From 539b6f262e53271617d28bcadb16385edc4a3ef3 Mon Sep 17 00:00:00 2001 From: Arnaud Nelissen Date: Tue, 30 Sep 2025 19:05:40 +0200 Subject: [PATCH] Ajout redirection des logs sur websocket et page /logs/view --- app.js | 11 +- logs-view/index.html | 464 +++++++++++++++++++++++++++++++++++++ package-lock.json | 24 +- package.json | 3 +- services/device.service.js | 15 +- services/index.js | 2 + services/log.service.js | 123 ++++++++++ 7 files changed, 633 insertions(+), 9 deletions(-) create mode 100644 logs-view/index.html create mode 100644 services/log.service.js diff --git a/app.js b/app.js index 734a14b..bcf725f 100644 --- a/app.js +++ b/app.js @@ -6,6 +6,7 @@ const express = require('express'); const fs = require('fs'); const http = require('http'); const https = require('https'); +const path = require('path'); require('dotenv').config() @@ -22,7 +23,7 @@ const credentials = { }; const dbcontroller = require('./db'); -const {firmware} = require('./services'); +const {firmware, log} = require('./services'); const routes = require('./routes') const app = express(); @@ -60,6 +61,11 @@ dbcontroller.init() console.log('MongoDB Client initialized'); console.log('-------------------------------------------------------'); + (async () => { + await log.attachLogService(httpServer, '/logs'); + await log.attachLogService(httpsServer, '/logs'); + })(); + ///// Startup HTTP Server httpServer.listen(http_port, () => { console.log('-------------------------------------------------------'); @@ -88,6 +94,9 @@ app.use(express.json()); app.use(express.raw({ type: 'application/cbor' })); app.use(express.urlencoded({ extended: true })); +// Serve the log viewer HTML +app.use('/logs/view', express.static(path.join(__dirname, 'logs-view'))); + app.use(function (req, res, next) { // Log request console.log('-------------------------------------------------------'); diff --git a/logs-view/index.html b/logs-view/index.html new file mode 100644 index 0000000..e204d24 --- /dev/null +++ b/logs-view/index.html @@ -0,0 +1,464 @@ + + + + +WebSocket Logs + + + + + + + + + +
+
+ + + + +
+ +
+ +
+ + +
+
+ +
+
+

Login

+ +
+ +
+
+ + + + diff --git a/package-lock.json b/package-lock.json index aaec6b6..6382b5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,8 @@ "mongodb": "^3.6.2", "oauth2-server": "^3.1.1", "random-bytes": "^1.0.0", - "underscore": "^1.13.6" + "underscore": "^1.13.6", + "ws": "^8.18.3" } }, "node_modules/@hapi/hoek": { @@ -1536,6 +1537,27 @@ "funding": { "url": "https://github.com/sponsors/ljharb" } + }, + "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 + } + } } } } diff --git a/package.json b/package.json index 5dc14a4..224034c 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "mongodb": "^3.6.2", "oauth2-server": "^3.1.1", "random-bytes": "^1.0.0", - "underscore": "^1.13.6" + "underscore": "^1.13.6", + "ws": "^8.18.3" } } diff --git a/services/device.service.js b/services/device.service.js index 673a4da..601379c 100644 --- a/services/device.service.js +++ b/services/device.service.js @@ -31,20 +31,23 @@ const getRequestToDo = async function (msn) { ///// Programs if (device.programs != undefined) { - if (child.programmingTimestamp < device.programs.timestamp) { todo.programs = 1 } - else if (child.programmingTimestamp > device.programs.timestamp) { todo.programs = 2 } + timestamp = child.programmingTimestamp || -1 + if (timestamp < device.programs.timestamp) { todo.programs = 1 } + else if (timestamp > device.programs.timestamp) { todo.programs = 2 } } ///// Configuration if (device.configuration != undefined) { - if (child.configurationTimestamp < device.configuration.timestamp) { todo.configuration = 1 } - else if (child.configurationTimestamp > device.configuration.timestamp) { todo.configuration = 2 } + timestamp = child.configurationTimestamp || child.relayConfigurationTimestamp || -1 + if (timestamp < device.configuration.timestamp) { todo.configuration = 1 } + else if (timestamp > device.configuration.timestamp) { todo.configuration = 2 } } ///// Slots if (device.slots != undefined) { - if (child.programmingTimestamp < device.slots.timestamp) { todo.slots = 1 } - else if (child.programmingTimestamp > device.slots.timestamp) { todo.slots = 2 } + timestamp = child.programmingTimestamp || -1 + if (timestamp < device.slots.timestamp) { todo.slots = 1 } + else if (timestamp > device.slots.timestamp) { todo.slots = 2 } } ///// Status diff --git a/services/index.js b/services/index.js index 15e0a07..0179814 100644 --- a/services/index.js +++ b/services/index.js @@ -6,6 +6,7 @@ const ipx = require('./ipx.service') const journal = require('./journal.service') const longpolling = require('./longpolling.service') const device = require('./device.service') +const log = require('./log.service') module.exports = { status, @@ -16,4 +17,5 @@ module.exports = { journal, longpolling, device, + log } \ No newline at end of file diff --git a/services/log.service.js b/services/log.service.js new file mode 100644 index 0000000..a207bf4 --- /dev/null +++ b/services/log.service.js @@ -0,0 +1,123 @@ +// services/logService.js +const WebSocket = require('ws'); + +const decodeURIComponentSafe = str => { + try { + return decodeURIComponent(str); + } catch { + return str; // fallback + } +}; + +const wssList = []; // all WS servers +const sockets = []; // { ws, name } +const originalConsole = {}; +let clients = new Map(); // ws => username + +// ASCII check +function isAscii(str) { + return /^[\x00-\x7F]*$/.test(str); +} + +const attachLogService = async function (server, basePath = '/logs') { + try { + const wss = new WebSocket.Server({ noServer: true }); + wssList.push(wss); + + // Save original console methods once + ["log", "error", "warn", "info", "debug"].forEach(fn => { + if (!originalConsole[fn]) originalConsole[fn] = console[fn]; + }); + + // Override console methods (only once) + if (!originalConsole._overridden) { + ["log", "error", "warn", "info", "debug"].forEach(fn => { + console[fn] = (...args) => { + originalConsole[fn].apply(console, args); + const msg = args.map(a => (typeof a === "string" ? a : JSON.stringify(a))).join(" "); + broadcast({ type: fn, message: msg, client: "server" }); + }; + }); + originalConsole._overridden = true; + } + + // Handle HTTP upgrade requests + server.on("upgrade", (request, socket, head) => { + const urlParts = request.url.split('/').filter(Boolean); // e.g. ['logs','😎'] + if (urlParts[0] === basePath.replace(/^\/+/, '') && urlParts[1]) { + // Decode URL safely, preserving emojis + const clientName = decodeURIComponentSafe(urlParts[1]); + + wss.handleUpgrade(request, socket, head, ws => { + ws.clientName = clientName; + wss.emit('connection', ws, request); + clients.set(ws, clientName); + sendClientsUpdate(); + }); + } else { + socket.destroy(); + } + }); + + // WS connection + wss.on('connection', ws => { + sockets.push(ws); + + ws.on("message", rawMsg => { + const msg = rawMsg.toString(); + broadcast({ type: "message", message: msg, client: ws.clientName }); + }); + + ws.on("close", () => { + const index = sockets.indexOf(ws); + if (index !== -1) sockets.splice(index, 1); + + clients.delete(ws); + sendClientsUpdate(); + }); + }); + + return { success: true }; + } catch (e) { + throw new Error(e.message); + } +}; + +const detachLogService = async function () { + try { + Object.keys(originalConsole).forEach(fn => { + if (fn !== "_overridden") console[fn] = originalConsole[fn]; + }); + + sockets.forEach(ws => ws.close()); + sockets.length = 0; + + wssList.forEach(wss => wss.close()); + wssList.length = 0; + + return { success: true }; + } catch (e) { + throw new Error(e.message); + } +}; + + +// Broadcast helper +function broadcast(obj) { + // Add timestamp + obj.timestamp = new Date().toISOString(); + const msg = JSON.stringify(obj); + sockets.forEach(ws => { + if (ws.readyState === WebSocket.OPEN) ws.send(msg); + }); +} + +function sendClientsUpdate() { + const clientNames = Array.from(clients.values()); + broadcast({ clients: clientNames }); +} + +module.exports = { + attachLogService, + detachLogService, +};