From bee134e6ecf73605b3ac215a8b17f11301078ce5 Mon Sep 17 00:00:00 2001 From: Emmo00 Date: Fri, 21 Nov 2025 18:52:47 +0100 Subject: [PATCH 01/13] feat: implement terminal authentication and WebSocket connection for terminal sessions --- package-lock.json | 218 +++++++++++++++++++++++++------------ package.json | 5 +- src/logic/context.ts | 101 ++++++++++++++++- src/public/css/global.css | 35 ++++++ src/public/js/terminal.js | 157 ++++++++++++++++++++++++++ src/public/js/toggle.js | 5 + src/views/index.hbs | 33 +++++- src/views/layouts/main.hbs | 3 + 8 files changed, 480 insertions(+), 77 deletions(-) create mode 100644 src/public/js/terminal.js diff --git a/package-lock.json b/package-lock.json index 3ee77e1..ec0ae02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,9 @@ "express": "^4.19.1", "express-handlebars": "^7.1.2", "handlebars": "^4.7.8", - "mqtt": "^5.5.0" + "mqtt": "^5.5.0", + "ssh2": "^1.17.0", + "ws": "^8.18.3" }, "devDependencies": { "@babel/preset-env": "^7.28.3", @@ -29,6 +31,7 @@ "@types/express": "^4.17.21", "@types/jest": "^30.0.0", "@types/node": "^20.11.30", + "@types/ssh2": "^1.15.5", "babel-jest": "^30.0.5", "jest": "^30.0.5", "nodemon": "^3.1.0", @@ -1985,6 +1988,27 @@ "xstream": "^11.14.0" } }, + "node_modules/@cosmjs/socket/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@cosmjs/stargate": { "version": "0.33.1", "resolved": "https://registry.npmjs.org/@cosmjs/stargate/-/stargate-0.33.1.tgz", @@ -4657,27 +4681,6 @@ "node": ">=6.14.2" } }, - "node_modules/@streamr/dht/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/@streamr/geoip-location": { "version": "103.1.1", "resolved": "https://registry.npmjs.org/@streamr/geoip-location/-/geoip-location-103.1.1.tgz", @@ -5155,6 +5158,33 @@ "@types/node": "*" } }, + "node_modules/@types/ssh2": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.5.tgz", + "integrity": "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18" + } + }, + "node_modules/@types/ssh2/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/ssh2/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -5729,6 +5759,15 @@ "arweave": "^1.10.0" } }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, "node_modules/asn1.js": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", @@ -6024,6 +6063,21 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bcrypt-pbkdf/node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, "node_modules/bech32": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", @@ -6319,6 +6373,15 @@ "node": ">=6.14.2" } }, + "node_modules/buildcheck": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", + "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==", + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -6948,6 +7011,20 @@ "integrity": "sha512-MN/yUe6mkJwHnCFfsNPeCfXVhyxHYW6c/xDUzrSbBycYzw++XvWDMJArXp2pLdgD6FQ8DW79vkPjeNKVrXaHeQ==", "license": "Apache-2.0" }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -8956,6 +9033,27 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/jayson/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/jest": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", @@ -11147,27 +11245,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/mqtt/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/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -11199,6 +11276,13 @@ "readable-stream": "^3.6.0" } }, + "node_modules/nan": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.1.tgz", + "integrity": "sha512-r7bBUGKzlqk8oPBDYxt6Z0aEdF1G1rwlMcLk8LCOMbOzf0mG+JUfUzG4fIMWwHWP0iyaLWEQZJmtB7nOHEm/qw==", + "license": "MIT", + "optional": true + }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", @@ -12754,27 +12838,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/rpc-websockets/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/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -13264,6 +13327,23 @@ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "license": "MIT" }, + "node_modules/ssh2": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", + "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==", + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.23.0" + } + }, "node_modules/ssri": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", @@ -14814,16 +14894,16 @@ } }, "node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "license": "MIT", "engines": { - "node": ">=8.3.0" + "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { diff --git a/package.json b/package.json index a3def09..3710264 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,9 @@ "express": "^4.19.1", "express-handlebars": "^7.1.2", "handlebars": "^4.7.8", - "mqtt": "^5.5.0" + "mqtt": "^5.5.0", + "ssh2": "^1.17.0", + "ws": "^8.18.3" }, "devDependencies": { "@babel/preset-env": "^7.28.3", @@ -33,6 +35,7 @@ "@types/express": "^4.17.21", "@types/jest": "^30.0.0", "@types/node": "^20.11.30", + "@types/ssh2": "^1.15.5", "babel-jest": "^30.0.5", "jest": "^30.0.5", "nodemon": "^3.1.0", diff --git a/src/logic/context.ts b/src/logic/context.ts index 9ddecaa..1c995bb 100644 --- a/src/logic/context.ts +++ b/src/logic/context.ts @@ -1,6 +1,9 @@ import { create } from "express-handlebars"; import express, { Express } from "express"; import { JsonRpcProvider, Contract } from "ethers"; +import WebSocket from "ws"; +import { ConnectConfig, Client as SSHClient } from "ssh2"; +import http from "http"; // HBS CONFIG const hbs = create({ @@ -15,6 +18,9 @@ const hbs = create({ // EXPRESS APP CONFIG export const app: Express = express(); +const server = http.createServer(app); +const wss = new WebSocket.Server({ server }); + app.engine("hbs", hbs.engine); app.set("view engine", "hbs"); app.set("views", "./src/views"); @@ -24,7 +30,100 @@ app.use(express.urlencoded({ extended: true })); const port = process.env.PORT || 3000; -app.listen(port, () => { +wss.on("connection", (ws: WebSocket, request: http.IncomingMessage) => { + console.log("[ws]: New WebSocket connection established"); + + console.log("url", request.url); + + const url = new URL(request.url!, "http://localhost"); + const cols = url.searchParams.get("cols") ?? "80"; + const rows = url.searchParams.get("rows") ?? "24"; + const username = url.searchParams.get("username") ?? "ubuntu"; + const password = url.searchParams.get("password") ?? "Mauchly92618"; + + const ssh = new SSHClient(); + const sshConfig: ConnectConfig = { + host: "127.0.0.1", + port: 22, + username, + password, + }; + + ssh + .on("ready", () => { + console.log("[ws/ssh]: SSH connection established"); + ssh.shell( + { term: "xterm-256", cols: parseInt(cols), rows: parseInt(rows) }, + (err, stream) => { + if (err) { + ws.send(`[ws/ssh]: SSH shell error: ${err.message}`); + ws.close(); + ssh.end(); + return; + } + + // SSH -> WebSocket + stream.on("data", (data: Buffer) => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(data); + } + }); + + stream.on("close", () => { + console.log("[ws/ssh]: SSH stream closed"); + ws.close(); + ssh.end(); + }); + + stream.stderr.on("data", (data: Buffer) => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(`[ws/ssh]: SSH stderr: ${data.toString()}`); + } + }); + + // WebSocket -> SSH + console.log("[ws/ssh]: Setting up WebSocket message handler"); + ws.on("message", (msg) => { + console.log("[ws]: Received message from WebSocket", msg.toString()); + // support JSON control messages for resize + try { + const parsed = JSON.parse(msg.toString()); + if (parsed.type === "resize") { + const { cols, rows } = parsed; + stream.setWindow(rows, cols, cols * 8, rows * 16); // width/height px optional + return; + } + } catch (e) { + /* not JSON - treat as raw data */ + } + + if (stream.writable) stream.write(msg); + }); + + ws.on("close", () => { + console.log("[ws]: WebSocket closed, closing SSH stream"); + stream.end(); + ssh.end(); + }); + + ws.on("error", () => { + console.log("[ws]: WebSocket error, closing SSH stream"); + stream.end(); + ssh.end(); + }); + } + ); + }) + .on("error", (err) => { + console.error("[ws/ssh]: SSH connection error:", err); + ws.send(`[ws/ssh]: SSH connection error: ${err.message}`); + ws.close(); + ssh.end(); + }) + .connect(sshConfig); +}); + +server.listen(port, () => { console.log(`[server]: Server is running at port ${port}`); }); diff --git a/src/public/css/global.css b/src/public/css/global.css index 602ffae..f895b92 100644 --- a/src/public/css/global.css +++ b/src/public/css/global.css @@ -35,4 +35,39 @@ iframe { .hidden { display: none; +} + +/* Terminal specific styles */ +#terminal-auth { + background-color: #212529 !important; +} + +#terminal-auth h4 { + color: #ffffff; + margin-bottom: 15px; +} + +#terminal-auth label { + color: #ffffff; + margin-bottom: 5px; + display: block; +} + +#terminal-container { + background-color: #000000 !important; + padding: 0 !important; +} + +#xterm-terminal { + padding: 10px; +} + +#terminal-status { + background-color: #212529 !important; +} + +#terminal-status p { + color: #ffffff; + margin: 10px 0; + font-size: 12px; } \ No newline at end of file diff --git a/src/public/js/terminal.js b/src/public/js/terminal.js new file mode 100644 index 0000000..18dccd3 --- /dev/null +++ b/src/public/js/terminal.js @@ -0,0 +1,157 @@ +// Terminal connection variables +let terminal = null; +let socket = null; +let isConnected = false; + +function connectTerminal(event) { + event.preventDefault(); + + const username = document.getElementById('username').value; + const password = document.getElementById('password').value; + const statusMessage = document.getElementById('status-message'); + const authForm = document.getElementById('terminal-auth'); + const terminalContainer = document.getElementById('terminal-container'); + + // Show connecting status + statusMessage.textContent = 'Connecting to terminal...'; + + // Initialize xterm terminal + if (!terminal) { + terminal = new Terminal({ + cursorBlink: true, + theme: { + background: '#212529', + foreground: '#ffffff' + } + }); + terminal.open(document.getElementById('xterm-terminal')); + } + + // Determine protocol + const protocol = (location.protocol === 'https:') ? 'wss' : 'ws'; + const cols = terminal.cols; + const rows = terminal.rows; + + // Create WebSocket connection with credentials and terminal size + const socketUrl = `${protocol}://${location.host}/?username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}&cols=${cols}&rows=${rows}`; + + socket = new WebSocket(socketUrl); + socket.binaryType = 'arraybuffer'; + + socket.onopen = () => { + isConnected = true; + statusMessage.textContent = 'Connected to terminal'; + authForm.style.display = 'none'; + terminalContainer.style.display = 'block'; + + terminal.write('Connected to M3tering Console Terminal.\r\n'); + + // Send initial resize + socket.send(JSON.stringify({ type: 'resize', cols: terminal.cols, rows: terminal.rows })); + + // Send the docker compose logs command after connection is established + setTimeout(() => { + terminal.write('\r\nExecuting: cd Console && docker compose logs\r\n'); + const command = "dir\n" ; 'cd Console && docker compose logs\r'; + socket.send(command); + }, 1500); + }; + + socket.onmessage = (event) => { + // Handle incoming terminal data + if (typeof event.data === 'string') { + terminal.write(event.data); + } else { + terminal.write(new Uint8Array(event.data)); + } + }; + + socket.onclose = () => { + isConnected = false; + statusMessage.textContent = 'Terminal connection closed'; + authForm.style.display = 'block'; + terminalContainer.style.display = 'none'; + + if (terminal) { + terminal.write('\r\nConnection closed.\r\n'); + } + }; + + socket.onerror = (error) => { + console.error('WebSocket error:', error); + statusMessage.textContent = 'Connection error. Please check credentials and try again.'; + authForm.style.display = 'block'; + terminalContainer.style.display = 'none'; + isConnected = false; + }; + + // Handle terminal input + if (terminal) { + terminal.onData(data => { + if (socket && socket.readyState === WebSocket.OPEN) { + socket.send(data); + } + }); + + // Handle terminal resize + terminal.onResize(({ cols, rows }) => { + if (socket && socket.readyState === WebSocket.OPEN) { + socket.send(JSON.stringify({ type: 'resize', cols, rows })); + } + }); + } + + // Handle window resize + window.addEventListener('resize', () => { + if (terminal && socket && socket.readyState === WebSocket.OPEN) { + socket.send(JSON.stringify({ type: 'resize', cols: terminal.cols, rows: terminal.rows })); + } + }); +} + +function disconnectTerminal() { + if (socket) { + socket.close(); + } + + const authForm = document.getElementById('terminal-auth'); + const terminalContainer = document.getElementById('terminal-container'); + const statusMessage = document.getElementById('status-message'); + + authForm.style.display = 'block'; + terminalContainer.style.display = 'none'; + statusMessage.textContent = 'Terminal disconnected'; + + if (terminal) { + terminal.clear(); + } + + isConnected = false; +} + +// Show terminal authentication form when terminal is opened +function showTerminalAuth() { + const authForm = document.getElementById('terminal-auth'); + const terminalContainer = document.getElementById('terminal-container'); + const statusMessage = document.getElementById('status-message'); + + // Reset UI to show auth form + authForm.style.display = 'block'; + terminalContainer.style.display = 'none'; + statusMessage.textContent = 'Enter credentials and click Connect to start terminal session'; + + // Focus on username field + document.getElementById('username').focus(); +} + +// Clean up when terminal app is closed +function cleanupTerminal() { + if (socket) { + socket.close(); + } + if (terminal) { + terminal.dispose(); + terminal = null; + } + isConnected = false; +} \ No newline at end of file diff --git a/src/public/js/toggle.js b/src/public/js/toggle.js index 6f0cdfc..c66c178 100644 --- a/src/public/js/toggle.js +++ b/src/public/js/toggle.js @@ -4,6 +4,11 @@ function toggleApp(appId, state) { var app = document.getElementById(appId); app.style.display = appState; + // Clean up terminal when closing + if (appId === 'terminal' && state === false && typeof cleanupTerminal === 'function') { + cleanupTerminal(); + } + ["m3ters-icon", "browser-icon", "terminal-icon", "paint-icon"].forEach( (iconId) => { var icon = document.getElementById(iconId); diff --git a/src/views/index.hbs b/src/views/index.hbs index c240192..c6ce685 100644 --- a/src/views/index.hbs +++ b/src/views/index.hbs @@ -76,7 +76,7 @@ @@ -189,12 +189,33 @@ > _ -
-
-

Loading M3ter data...

-
+ + {{! Authentication Form }} +
+

Terminal Authentication

+
+
+ + +
+
+ + +
+ + +
+
+ + {{! Terminal Container }} + + + {{! Status Messages }} +
+

Enter credentials and click Connect to start terminal session

-
diff --git a/src/views/layouts/main.hbs b/src/views/layouts/main.hbs index e8e0685..9c91081 100644 --- a/src/views/layouts/main.hbs +++ b/src/views/layouts/main.hbs @@ -10,11 +10,14 @@ href="https://unpkg.com/nes.css@latest/css/nes.min.css" rel="stylesheet" /> + {{{body}}} + + \ No newline at end of file From 4ca800bed5f07396e222c11c9ad491fb3ce61526 Mon Sep 17 00:00:00 2001 From: Emmo00 Date: Fri, 21 Nov 2025 20:55:14 +0100 Subject: [PATCH 02/13] feat: enhance terminal UI with responsive sizing and fit button --- src/public/css/global.css | 5 ++ src/public/js/terminal.js | 116 ++++++++++++++++++++++++++++---------- src/views/index.hbs | 1 + 3 files changed, 91 insertions(+), 31 deletions(-) diff --git a/src/public/css/global.css b/src/public/css/global.css index f895b92..6915a36 100644 --- a/src/public/css/global.css +++ b/src/public/css/global.css @@ -56,10 +56,15 @@ iframe { #terminal-container { background-color: #000000 !important; padding: 0 !important; + width: 100%; + min-height: 400px; } #xterm-terminal { padding: 10px; + width: calc(100% - 20px); + height: calc(100% - 20px); + min-height: 380px; } #terminal-status { diff --git a/src/public/js/terminal.js b/src/public/js/terminal.js index 18dccd3..1c79e38 100644 --- a/src/public/js/terminal.js +++ b/src/public/js/terminal.js @@ -15,7 +15,11 @@ function connectTerminal(event) { // Show connecting status statusMessage.textContent = 'Connecting to terminal...'; - // Initialize xterm terminal + // Show terminal container first so we can get proper dimensions + authForm.style.display = 'none'; + terminalContainer.style.display = 'block'; + + // Initialize xterm terminal with proper sizing if (!terminal) { terminal = new Terminal({ cursorBlink: true, @@ -25,37 +29,47 @@ function connectTerminal(event) { } }); terminal.open(document.getElementById('xterm-terminal')); + + // Fit terminal to container size + setTimeout(() => { + fitTerminalToContainer(); + }, 100); } - // Determine protocol + // Determine protocol - get dimensions after terminal is sized const protocol = (location.protocol === 'https:') ? 'wss' : 'ws'; - const cols = terminal.cols; - const rows = terminal.rows; - // Create WebSocket connection with credentials and terminal size - const socketUrl = `${protocol}://${location.host}/?username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}&cols=${cols}&rows=${rows}`; - - socket = new WebSocket(socketUrl); - socket.binaryType = 'arraybuffer'; - - socket.onopen = () => { - isConnected = true; - statusMessage.textContent = 'Connected to terminal'; - authForm.style.display = 'none'; - terminalContainer.style.display = 'block'; + // Wait a moment for terminal to be properly sized, then connect + setTimeout(() => { + const cols = terminal.cols; + const rows = terminal.rows; - terminal.write('Connected to M3tering Console Terminal.\r\n'); + // Create WebSocket connection with credentials and terminal size + const socketUrl = `${protocol}://${location.host}/?username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}&cols=${cols}&rows=${rows}`; - // Send initial resize - socket.send(JSON.stringify({ type: 'resize', cols: terminal.cols, rows: terminal.rows })); + console.log(`Connecting with terminal size: ${cols}x${rows}`); - // Send the docker compose logs command after connection is established - setTimeout(() => { - terminal.write('\r\nExecuting: cd Console && docker compose logs\r\n'); - const command = "dir\n" ; 'cd Console && docker compose logs\r'; - socket.send(command); - }, 1500); - }; + socket = new WebSocket(socketUrl); + socket.binaryType = 'arraybuffer'; + + socket.onopen = () => { + isConnected = true; + statusMessage.textContent = `Connected to terminal (${terminal.cols}x${terminal.rows})`; + + terminal.write('Connected to M3tering Console Terminal.\r\n'); + terminal.write(`Terminal size: ${terminal.cols} columns x ${terminal.rows} rows\r\n`); + + // Send initial resize with current dimensions + socket.send(JSON.stringify({ type: 'resize', cols: terminal.cols, rows: terminal.rows })); + + // Send the docker compose logs command after connection is established + setTimeout(() => { + terminal.write('\r\nExecuting: cd Console && docker compose logs\r\n'); + const command = 'cd Console && docker compose logs\r'; + socket.send(command); + }, 1500); + }; + }, 200); socket.onmessage = (event) => { // Handle incoming terminal data @@ -101,12 +115,8 @@ function connectTerminal(event) { }); } - // Handle window resize - window.addEventListener('resize', () => { - if (terminal && socket && socket.readyState === WebSocket.OPEN) { - socket.send(JSON.stringify({ type: 'resize', cols: terminal.cols, rows: terminal.rows })); - } - }); + // Handle window resize - remove the event listener since we'll add it globally + // (This prevents multiple listeners from being added) } function disconnectTerminal() { @@ -129,6 +139,40 @@ function disconnectTerminal() { isConnected = false; } +// Manually fit terminal to container +function fitTerminalToContainer() { + if (terminal) { + const container = document.getElementById('xterm-terminal'); + if (container && container.offsetParent !== null) { + // Use a temporary character measurement + const testElement = document.createElement('div'); + testElement.style.visibility = 'hidden'; + testElement.style.position = 'absolute'; + testElement.style.fontFamily = 'Monaco, Menlo, "Ubuntu Mono", monospace'; + testElement.style.fontSize = '13px'; + testElement.textContent = '0'; + document.body.appendChild(testElement); + + const charWidth = testElement.offsetWidth; + const charHeight = testElement.offsetHeight; + document.body.removeChild(testElement); + + const containerRect = container.getBoundingClientRect(); + const cols = Math.floor((containerRect.width - 20) / charWidth); + const rows = Math.floor((containerRect.height - 20) / charHeight); + + if (cols > 10 && rows > 5) { + console.log(`Fitting terminal to container: ${cols}x${rows} (char: ${charWidth}x${charHeight}px)`); + terminal.resize(cols, rows); + + if (socket && socket.readyState === WebSocket.OPEN) { + socket.send(JSON.stringify({ type: 'resize', cols: cols, rows: rows })); + } + } + } + } +} + // Show terminal authentication form when terminal is opened function showTerminalAuth() { const authForm = document.getElementById('terminal-auth'); @@ -144,6 +188,16 @@ function showTerminalAuth() { document.getElementById('username').focus(); } +// Global resize handler +function handleTerminalResize() { + if (terminal && isConnected) { + fitTerminalToContainer(); + } +} + +// Add global window resize listener +window.addEventListener('resize', handleTerminalResize); + // Clean up when terminal app is closed function cleanupTerminal() { if (socket) { diff --git a/src/views/index.hbs b/src/views/index.hbs index c6ce685..958335c 100644 --- a/src/views/index.hbs +++ b/src/views/index.hbs @@ -204,6 +204,7 @@ + From 9806e6f4ef3315056258b74a49ebb00006ae5ea3 Mon Sep 17 00:00:00 2001 From: Emmo00 Date: Fri, 21 Nov 2025 21:13:34 +0100 Subject: [PATCH 03/13] Revert "feat: enhance terminal UI with responsive sizing and fit button" This reverts commit 4ca800bed5f07396e222c11c9ad491fb3ce61526. --- src/public/css/global.css | 5 -- src/public/js/terminal.js | 116 ++++++++++---------------------------- src/views/index.hbs | 1 - 3 files changed, 31 insertions(+), 91 deletions(-) diff --git a/src/public/css/global.css b/src/public/css/global.css index 6915a36..f895b92 100644 --- a/src/public/css/global.css +++ b/src/public/css/global.css @@ -56,15 +56,10 @@ iframe { #terminal-container { background-color: #000000 !important; padding: 0 !important; - width: 100%; - min-height: 400px; } #xterm-terminal { padding: 10px; - width: calc(100% - 20px); - height: calc(100% - 20px); - min-height: 380px; } #terminal-status { diff --git a/src/public/js/terminal.js b/src/public/js/terminal.js index 1c79e38..18dccd3 100644 --- a/src/public/js/terminal.js +++ b/src/public/js/terminal.js @@ -15,11 +15,7 @@ function connectTerminal(event) { // Show connecting status statusMessage.textContent = 'Connecting to terminal...'; - // Show terminal container first so we can get proper dimensions - authForm.style.display = 'none'; - terminalContainer.style.display = 'block'; - - // Initialize xterm terminal with proper sizing + // Initialize xterm terminal if (!terminal) { terminal = new Terminal({ cursorBlink: true, @@ -29,47 +25,37 @@ function connectTerminal(event) { } }); terminal.open(document.getElementById('xterm-terminal')); - - // Fit terminal to container size - setTimeout(() => { - fitTerminalToContainer(); - }, 100); } - // Determine protocol - get dimensions after terminal is sized + // Determine protocol const protocol = (location.protocol === 'https:') ? 'wss' : 'ws'; + const cols = terminal.cols; + const rows = terminal.rows; - // Wait a moment for terminal to be properly sized, then connect - setTimeout(() => { - const cols = terminal.cols; - const rows = terminal.rows; - - // Create WebSocket connection with credentials and terminal size - const socketUrl = `${protocol}://${location.host}/?username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}&cols=${cols}&rows=${rows}`; + // Create WebSocket connection with credentials and terminal size + const socketUrl = `${protocol}://${location.host}/?username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}&cols=${cols}&rows=${rows}`; + + socket = new WebSocket(socketUrl); + socket.binaryType = 'arraybuffer'; + + socket.onopen = () => { + isConnected = true; + statusMessage.textContent = 'Connected to terminal'; + authForm.style.display = 'none'; + terminalContainer.style.display = 'block'; - console.log(`Connecting with terminal size: ${cols}x${rows}`); + terminal.write('Connected to M3tering Console Terminal.\r\n'); - socket = new WebSocket(socketUrl); - socket.binaryType = 'arraybuffer'; + // Send initial resize + socket.send(JSON.stringify({ type: 'resize', cols: terminal.cols, rows: terminal.rows })); - socket.onopen = () => { - isConnected = true; - statusMessage.textContent = `Connected to terminal (${terminal.cols}x${terminal.rows})`; - - terminal.write('Connected to M3tering Console Terminal.\r\n'); - terminal.write(`Terminal size: ${terminal.cols} columns x ${terminal.rows} rows\r\n`); - - // Send initial resize with current dimensions - socket.send(JSON.stringify({ type: 'resize', cols: terminal.cols, rows: terminal.rows })); - - // Send the docker compose logs command after connection is established - setTimeout(() => { - terminal.write('\r\nExecuting: cd Console && docker compose logs\r\n'); - const command = 'cd Console && docker compose logs\r'; - socket.send(command); - }, 1500); - }; - }, 200); + // Send the docker compose logs command after connection is established + setTimeout(() => { + terminal.write('\r\nExecuting: cd Console && docker compose logs\r\n'); + const command = "dir\n" ; 'cd Console && docker compose logs\r'; + socket.send(command); + }, 1500); + }; socket.onmessage = (event) => { // Handle incoming terminal data @@ -115,8 +101,12 @@ function connectTerminal(event) { }); } - // Handle window resize - remove the event listener since we'll add it globally - // (This prevents multiple listeners from being added) + // Handle window resize + window.addEventListener('resize', () => { + if (terminal && socket && socket.readyState === WebSocket.OPEN) { + socket.send(JSON.stringify({ type: 'resize', cols: terminal.cols, rows: terminal.rows })); + } + }); } function disconnectTerminal() { @@ -139,40 +129,6 @@ function disconnectTerminal() { isConnected = false; } -// Manually fit terminal to container -function fitTerminalToContainer() { - if (terminal) { - const container = document.getElementById('xterm-terminal'); - if (container && container.offsetParent !== null) { - // Use a temporary character measurement - const testElement = document.createElement('div'); - testElement.style.visibility = 'hidden'; - testElement.style.position = 'absolute'; - testElement.style.fontFamily = 'Monaco, Menlo, "Ubuntu Mono", monospace'; - testElement.style.fontSize = '13px'; - testElement.textContent = '0'; - document.body.appendChild(testElement); - - const charWidth = testElement.offsetWidth; - const charHeight = testElement.offsetHeight; - document.body.removeChild(testElement); - - const containerRect = container.getBoundingClientRect(); - const cols = Math.floor((containerRect.width - 20) / charWidth); - const rows = Math.floor((containerRect.height - 20) / charHeight); - - if (cols > 10 && rows > 5) { - console.log(`Fitting terminal to container: ${cols}x${rows} (char: ${charWidth}x${charHeight}px)`); - terminal.resize(cols, rows); - - if (socket && socket.readyState === WebSocket.OPEN) { - socket.send(JSON.stringify({ type: 'resize', cols: cols, rows: rows })); - } - } - } - } -} - // Show terminal authentication form when terminal is opened function showTerminalAuth() { const authForm = document.getElementById('terminal-auth'); @@ -188,16 +144,6 @@ function showTerminalAuth() { document.getElementById('username').focus(); } -// Global resize handler -function handleTerminalResize() { - if (terminal && isConnected) { - fitTerminalToContainer(); - } -} - -// Add global window resize listener -window.addEventListener('resize', handleTerminalResize); - // Clean up when terminal app is closed function cleanupTerminal() { if (socket) { diff --git a/src/views/index.hbs b/src/views/index.hbs index 958335c..c6ce685 100644 --- a/src/views/index.hbs +++ b/src/views/index.hbs @@ -204,7 +204,6 @@ - From 5d108c035228f268f0da8cd5fc6744420ef4dc98 Mon Sep 17 00:00:00 2001 From: Emmo00 Date: Fri, 21 Nov 2025 21:19:07 +0100 Subject: [PATCH 04/13] feat: refactor terminal connection logic for improved readability and maintainability --- src/public/js/terminal.js | 159 +++++++++++++++++++------------------- 1 file changed, 78 insertions(+), 81 deletions(-) diff --git a/src/public/js/terminal.js b/src/public/js/terminal.js index 18dccd3..03f2314 100644 --- a/src/public/js/terminal.js +++ b/src/public/js/terminal.js @@ -5,106 +5,103 @@ let isConnected = false; function connectTerminal(event) { event.preventDefault(); - - const username = document.getElementById('username').value; - const password = document.getElementById('password').value; - const statusMessage = document.getElementById('status-message'); - const authForm = document.getElementById('terminal-auth'); - const terminalContainer = document.getElementById('terminal-container'); - + + const username = document.getElementById("username").value; + const password = document.getElementById("password").value; + const statusMessage = document.getElementById("status-message"); + const authForm = document.getElementById("terminal-auth"); + const terminalContainer = document.getElementById("terminal-container"); + // Show connecting status - statusMessage.textContent = 'Connecting to terminal...'; - - // Initialize xterm terminal - if (!terminal) { - terminal = new Terminal({ - cursorBlink: true, - theme: { - background: '#212529', - foreground: '#ffffff' - } - }); - terminal.open(document.getElementById('xterm-terminal')); - } - + statusMessage.textContent = "Connecting to terminal..."; + // Determine protocol - const protocol = (location.protocol === 'https:') ? 'wss' : 'ws'; + const protocol = location.protocol === "https:" ? "wss" : "ws"; const cols = terminal.cols; const rows = terminal.rows; - + // Create WebSocket connection with credentials and terminal size - const socketUrl = `${protocol}://${location.host}/?username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}&cols=${cols}&rows=${rows}`; - + const socketUrl = `${protocol}://${location.host}/?username=${encodeURIComponent( + username + )}&password=${encodeURIComponent(password)}&cols=${cols}&rows=${rows}`; + socket = new WebSocket(socketUrl); - socket.binaryType = 'arraybuffer'; - + socket.binaryType = "arraybuffer"; + socket.onopen = () => { isConnected = true; - statusMessage.textContent = 'Connected to terminal'; - authForm.style.display = 'none'; - terminalContainer.style.display = 'block'; - - terminal.write('Connected to M3tering Console Terminal.\r\n'); - - // Send initial resize - socket.send(JSON.stringify({ type: 'resize', cols: terminal.cols, rows: terminal.rows })); - - // Send the docker compose logs command after connection is established + statusMessage.textContent = "Connected to terminal"; + authForm.style.display = "none"; + terminalContainer.style.display = "block"; + setTimeout(() => { - terminal.write('\r\nExecuting: cd Console && docker compose logs\r\n'); - const command = "dir\n" ; 'cd Console && docker compose logs\r'; - socket.send(command); - }, 1500); + // Initialize xterm terminal + if (!terminal) { + terminal = new Terminal({ + cursorBlink: true, + theme: { + background: "#212529", + foreground: "#ffffff", + }, + }); + terminal.open(document.getElementById("xterm-terminal")); + } + + terminal.write("Connected to M3tering Console Terminal.\r\n"); + + // Send initial resize + socket.send(JSON.stringify({ type: "resize", cols: terminal.cols, rows: terminal.rows })); + }, 200); }; - + socket.onmessage = (event) => { // Handle incoming terminal data - if (typeof event.data === 'string') { + if (typeof event.data === "string") { terminal.write(event.data); } else { terminal.write(new Uint8Array(event.data)); } }; - + socket.onclose = () => { isConnected = false; - statusMessage.textContent = 'Terminal connection closed'; - authForm.style.display = 'block'; - terminalContainer.style.display = 'none'; - + statusMessage.textContent = "Terminal connection closed"; + authForm.style.display = "block"; + terminalContainer.style.display = "none"; + if (terminal) { - terminal.write('\r\nConnection closed.\r\n'); + terminal.write("\r\nConnection closed.\r\n"); } }; - + socket.onerror = (error) => { - console.error('WebSocket error:', error); - statusMessage.textContent = 'Connection error. Please check credentials and try again.'; - authForm.style.display = 'block'; - terminalContainer.style.display = 'none'; + console.error("WebSocket error:", error); + statusMessage.textContent = "Connection error. Please check credentials and try again."; + authForm.style.display = "block"; + terminalContainer.style.display = "none"; isConnected = false; }; - + // Handle terminal input if (terminal) { - terminal.onData(data => { + terminal.onData((data) => { if (socket && socket.readyState === WebSocket.OPEN) { socket.send(data); } }); - + // Handle terminal resize terminal.onResize(({ cols, rows }) => { if (socket && socket.readyState === WebSocket.OPEN) { - socket.send(JSON.stringify({ type: 'resize', cols, rows })); + socket.send(JSON.stringify({ type: "resize", cols, rows })); } }); } - + // Handle window resize - window.addEventListener('resize', () => { + window.addEventListener("resize", () => { if (terminal && socket && socket.readyState === WebSocket.OPEN) { - socket.send(JSON.stringify({ type: 'resize', cols: terminal.cols, rows: terminal.rows })); + socket.send(JSON.stringify({ type: "resize", cols: terminal.cols, rows: terminal.rows })); } }); } @@ -113,35 +110,35 @@ function disconnectTerminal() { if (socket) { socket.close(); } - - const authForm = document.getElementById('terminal-auth'); - const terminalContainer = document.getElementById('terminal-container'); - const statusMessage = document.getElementById('status-message'); - - authForm.style.display = 'block'; - terminalContainer.style.display = 'none'; - statusMessage.textContent = 'Terminal disconnected'; - + + const authForm = document.getElementById("terminal-auth"); + const terminalContainer = document.getElementById("terminal-container"); + const statusMessage = document.getElementById("status-message"); + + authForm.style.display = "block"; + terminalContainer.style.display = "none"; + statusMessage.textContent = "Terminal disconnected"; + if (terminal) { terminal.clear(); } - + isConnected = false; } // Show terminal authentication form when terminal is opened function showTerminalAuth() { - const authForm = document.getElementById('terminal-auth'); - const terminalContainer = document.getElementById('terminal-container'); - const statusMessage = document.getElementById('status-message'); - + const authForm = document.getElementById("terminal-auth"); + const terminalContainer = document.getElementById("terminal-container"); + const statusMessage = document.getElementById("status-message"); + // Reset UI to show auth form - authForm.style.display = 'block'; - terminalContainer.style.display = 'none'; - statusMessage.textContent = 'Enter credentials and click Connect to start terminal session'; - + authForm.style.display = "block"; + terminalContainer.style.display = "none"; + statusMessage.textContent = "Enter credentials and click Connect to start terminal session"; + // Focus on username field - document.getElementById('username').focus(); + document.getElementById("username").focus(); } // Clean up when terminal app is closed @@ -154,4 +151,4 @@ function cleanupTerminal() { terminal = null; } isConnected = false; -} \ No newline at end of file +} From 8da2cc42c0ee4a64d891cc027e5bee5e4bf6fc9c Mon Sep 17 00:00:00 2001 From: Emmo00 Date: Fri, 21 Nov 2025 21:47:11 +0100 Subject: [PATCH 05/13] ... --- src/public/js/terminal.js | 159 +++++++++++++++++++------------------- 1 file changed, 81 insertions(+), 78 deletions(-) diff --git a/src/public/js/terminal.js b/src/public/js/terminal.js index 03f2314..92fa497 100644 --- a/src/public/js/terminal.js +++ b/src/public/js/terminal.js @@ -5,103 +5,106 @@ let isConnected = false; function connectTerminal(event) { event.preventDefault(); - - const username = document.getElementById("username").value; - const password = document.getElementById("password").value; - const statusMessage = document.getElementById("status-message"); - const authForm = document.getElementById("terminal-auth"); - const terminalContainer = document.getElementById("terminal-container"); - + + const username = document.getElementById('username').value; + const password = document.getElementById('password').value; + const statusMessage = document.getElementById('status-message'); + const authForm = document.getElementById('terminal-auth'); + const terminalContainer = document.getElementById('terminal-container'); + // Show connecting status - statusMessage.textContent = "Connecting to terminal..."; - + statusMessage.textContent = 'Connecting to terminal...'; + + // Initialize xterm terminal + if (!terminal) { + terminal = new Terminal({ + cursorBlink: true, + theme: { + background: '#212529', + foreground: '#ffffff' + } + }); + terminal.open(document.getElementById('xterm-terminal')); + } + // Determine protocol - const protocol = location.protocol === "https:" ? "wss" : "ws"; + const protocol = (location.protocol === 'https:') ? 'wss' : 'ws'; const cols = terminal.cols; const rows = terminal.rows; - + // Create WebSocket connection with credentials and terminal size - const socketUrl = `${protocol}://${location.host}/?username=${encodeURIComponent( - username - )}&password=${encodeURIComponent(password)}&cols=${cols}&rows=${rows}`; - + const socketUrl = `${protocol}://${location.host}/?username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}&cols=${cols}&rows=${rows}`; + socket = new WebSocket(socketUrl); - socket.binaryType = "arraybuffer"; - + socket.binaryType = 'arraybuffer'; + socket.onopen = () => { isConnected = true; - statusMessage.textContent = "Connected to terminal"; - authForm.style.display = "none"; - terminalContainer.style.display = "block"; - + statusMessage.textContent = 'Connected to terminal'; + authForm.style.display = 'none'; + terminalContainer.style.display = 'block'; + + terminal.write('Connected to M3tering Console Terminal.\r\n'); + + // Send initial resize + socket.send(JSON.stringify({ type: 'resize', cols: terminal.cols, rows: terminal.rows })); + + // Send the docker compose logs command after connection is established setTimeout(() => { - // Initialize xterm terminal - if (!terminal) { - terminal = new Terminal({ - cursorBlink: true, - theme: { - background: "#212529", - foreground: "#ffffff", - }, - }); - terminal.open(document.getElementById("xterm-terminal")); - } - - terminal.write("Connected to M3tering Console Terminal.\r\n"); - - // Send initial resize - socket.send(JSON.stringify({ type: "resize", cols: terminal.cols, rows: terminal.rows })); - }, 200); + terminal.write('\r\nExecuting: cd Console && docker compose logs\r\n'); + const command = "dir\n" ; 'cd Console && docker compose logs\r'; + // socket.send(command); + }, 1500); }; - + socket.onmessage = (event) => { // Handle incoming terminal data - if (typeof event.data === "string") { + if (typeof event.data === 'string') { terminal.write(event.data); } else { terminal.write(new Uint8Array(event.data)); } }; - + socket.onclose = () => { isConnected = false; - statusMessage.textContent = "Terminal connection closed"; - authForm.style.display = "block"; - terminalContainer.style.display = "none"; - + statusMessage.textContent = 'Terminal connection closed'; + authForm.style.display = 'block'; + terminalContainer.style.display = 'none'; + if (terminal) { - terminal.write("\r\nConnection closed.\r\n"); + terminal.write('\r\nConnection closed.\r\n'); } }; - + socket.onerror = (error) => { - console.error("WebSocket error:", error); - statusMessage.textContent = "Connection error. Please check credentials and try again."; - authForm.style.display = "block"; - terminalContainer.style.display = "none"; + console.error('WebSocket error:', error); + statusMessage.textContent = 'Connection error. Please check credentials and try again.'; + authForm.style.display = 'block'; + terminalContainer.style.display = 'none'; isConnected = false; }; - + // Handle terminal input if (terminal) { - terminal.onData((data) => { + terminal.onData(data => { if (socket && socket.readyState === WebSocket.OPEN) { socket.send(data); } }); - + // Handle terminal resize terminal.onResize(({ cols, rows }) => { if (socket && socket.readyState === WebSocket.OPEN) { - socket.send(JSON.stringify({ type: "resize", cols, rows })); + socket.send(JSON.stringify({ type: 'resize', cols, rows })); } }); } - + // Handle window resize - window.addEventListener("resize", () => { + window.addEventListener('resize', () => { if (terminal && socket && socket.readyState === WebSocket.OPEN) { - socket.send(JSON.stringify({ type: "resize", cols: terminal.cols, rows: terminal.rows })); + socket.send(JSON.stringify({ type: 'resize', cols: terminal.cols, rows: terminal.rows })); } }); } @@ -110,35 +113,35 @@ function disconnectTerminal() { if (socket) { socket.close(); } - - const authForm = document.getElementById("terminal-auth"); - const terminalContainer = document.getElementById("terminal-container"); - const statusMessage = document.getElementById("status-message"); - - authForm.style.display = "block"; - terminalContainer.style.display = "none"; - statusMessage.textContent = "Terminal disconnected"; - + + const authForm = document.getElementById('terminal-auth'); + const terminalContainer = document.getElementById('terminal-container'); + const statusMessage = document.getElementById('status-message'); + + authForm.style.display = 'block'; + terminalContainer.style.display = 'none'; + statusMessage.textContent = 'Terminal disconnected'; + if (terminal) { terminal.clear(); } - + isConnected = false; } // Show terminal authentication form when terminal is opened function showTerminalAuth() { - const authForm = document.getElementById("terminal-auth"); - const terminalContainer = document.getElementById("terminal-container"); - const statusMessage = document.getElementById("status-message"); - + const authForm = document.getElementById('terminal-auth'); + const terminalContainer = document.getElementById('terminal-container'); + const statusMessage = document.getElementById('status-message'); + // Reset UI to show auth form - authForm.style.display = "block"; - terminalContainer.style.display = "none"; - statusMessage.textContent = "Enter credentials and click Connect to start terminal session"; - + authForm.style.display = 'block'; + terminalContainer.style.display = 'none'; + statusMessage.textContent = 'Enter credentials and click Connect to start terminal session'; + // Focus on username field - document.getElementById("username").focus(); + document.getElementById('username').focus(); } // Clean up when terminal app is closed @@ -151,4 +154,4 @@ function cleanupTerminal() { terminal = null; } isConnected = false; -} +} \ No newline at end of file From 90d59bd0d95bc44ca6ffca899cd15d598b449e76 Mon Sep 17 00:00:00 2001 From: Emmo00 Date: Sun, 23 Nov 2025 03:02:34 +0100 Subject: [PATCH 06/13] feat: update Dockerfile and .dockerignore for improved build process and test file handling --- .dockerignore | 6 +++++- Dockerfile | 11 +++++------ package.json | 3 +-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.dockerignore b/.dockerignore index 6aee601..ea3d8cc 100644 --- a/.dockerignore +++ b/.dockerignore @@ -16,4 +16,8 @@ dist build *.log .DS_Store -Thumbs.db \ No newline at end of file +*.db + +# ignore test files +**/*.test.ts +**/*.spec.ts diff --git a/Dockerfile b/Dockerfile index 21bcad3..1d84708 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,17 +5,16 @@ WORKDIR /opt/app RUN apk add --no-cache cmake make g++ python3 openssl-dev py3-setuptools -# Optional: clean old node_modules if re-building -RUN rm -rf node_modules package-lock.json - # Copy and install dependencies -COPY package.json . -COPY package-lock.json . +COPY package*.json ./ + +RUN npm install --include=dev && npm cache clean --force + +# Copy application files COPY babel.config.js . COPY tsconfig.json . COPY .env . COPY src ./src -RUN npm install --include=dev # Build project RUN npm run build diff --git a/package.json b/package.json index 3710264..0451369 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,7 @@ "start": "node dist/index.js", "build": "tsc", "dev": "nodemon --exec ts-node src/index.ts", - "test": "tsc --noEmit && jest", - "rebuild": "npm rebuild" + "test": "tsc --noEmit && jest" }, "author": "Emmo00", "license": "MIT", From 5e9aa5a00e5c381165457218572bda95d7a374a1 Mon Sep 17 00:00:00 2001 From: Emmo00 Date: Mon, 15 Dec 2025 05:58:06 +0100 Subject: [PATCH 07/13] feat: enhance application initialization and add heartbeat publishing to Streamr --- src/index.ts | 33 +++++++-------------------------- src/logic/streamr.ts | 9 +++++++++ 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/src/index.ts b/src/index.ts index 1f411a0..0f6dda1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,18 +2,15 @@ import "dotenv/config"; import { handleUplinks } from "./logic/mqtt"; import { Request, Response } from "express"; import { app } from "./logic/context"; -import setupDatabase, { - getAllMeterRecords, - deleteMeterByPublicKey, -} from "./store/sqlite"; +import setupDatabase, { getAllMeterRecords, deleteMeterByPublicKey } from "./store/sqlite"; import { initializeVerifiersCache } from "./logic/sync"; -import "./logic/streamr"; +import { publishHeartbeatToStream } from "./logic/streamr"; // Async initialization function async function initializeApp() { try { console.log("[info] Starting application initialization..."); - + // Initialize database tables and jobs setupDatabase(); console.log("[info] Database setup completed"); @@ -21,11 +18,13 @@ async function initializeApp() { // Initialize verifiers cache on startup await initializeVerifiersCache(); console.log("[info] Verifiers cache initialized successfully"); - + // Start MQTT handling handleUplinks(); console.log("[info] MQTT uplinks handler started"); - + + await publishHeartbeatToStream(); + console.log("[info] Application initialization completed successfully"); } catch (error) { console.error("[fatal] Failed to initialize application:", error); @@ -42,24 +41,6 @@ app.get("/", async (req: Request, res: Response) => { console.log("[server]: Server handled GET request at `/`"); }); -app.post("/", async (req: Request, res: Response) => { - // try { - // const tokenId = (await req.body).tokenId; - // const publicKey = await m3ter.publicKey(tokenId); - // const latestNonce = await rollup.nonce(tokenId); - // saveMeter({ - // publicKey, - // tokenId, - // latestNonce: Number(latestNonce), - // devEui: (await req.body).devEui ?? null, - // }); - // } catch (err) { - // console.error(err); - // } - res.redirect("/"); - console.log("[server]: Server handled POST request at `/`"); -}); - app.delete("/delete-meter", async (req: Request, res: Response) => { let publicKey = decodeURIComponent(req.query?.publicKey?.toString() as string); if (publicKey) { diff --git a/src/logic/streamr.ts b/src/logic/streamr.ts index 344a338..45ba21c 100644 --- a/src/logic/streamr.ts +++ b/src/logic/streamr.ts @@ -26,6 +26,15 @@ async function getStream() { return await stream; } +export async function publishHeartbeatToStream() { + const stream = await getStream(); + const heartbeatPayload = { + timestamp: new Date().toISOString(), + }; + await stream.publish(heartbeatPayload); + console.log("[Streamr] Published heartbeat:", heartbeatPayload); +} + async function publishToStream(data: any) { const stream = await getStream(); await stream.publish(data); From 2f3645c77ae840021bd33db398c988535a40f326 Mon Sep 17 00:00:00 2001 From: Emmo00 Date: Mon, 15 Dec 2025 06:11:43 +0100 Subject: [PATCH 08/13] feat: log success message after sending pending transactions to Streamr --- src/logic/mqtt.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/logic/mqtt.ts b/src/logic/mqtt.ts index 949092b..8dafc2c 100644 --- a/src/logic/mqtt.ts +++ b/src/logic/mqtt.ts @@ -277,6 +277,7 @@ export async function handleMessage(blob: Buffer) { try { logger.info(`Sending pending transactions to streamr`); await publishPendingTransactionsToStreamr(pendingTransactions); + logger.info(`Successfully sent pending transactions to streamr`); } catch (error) { logger.error(`Error sending pending transactions to streamr: ${error}`); } From 4c638cc9f6ab23f9f2d9bd94aa40bd496cdf7cf7 Mon Sep 17 00:00:00 2001 From: Emmo00 Date: Mon, 15 Dec 2025 06:32:47 +0100 Subject: [PATCH 09/13] feat: enhance terminal container styles and initialize xterm with responsive resizing --- src/public/css/global.css | 21 ++++++++- src/public/js/terminal.js | 90 ++++++++++++++++++++++++++++++--------- src/views/index.hbs | 4 +- 3 files changed, 92 insertions(+), 23 deletions(-) diff --git a/src/public/css/global.css b/src/public/css/global.css index f895b92..86de9a5 100644 --- a/src/public/css/global.css +++ b/src/public/css/global.css @@ -56,10 +56,29 @@ iframe { #terminal-container { background-color: #000000 !important; padding: 0 !important; + height: 700px; + min-height: 700px; + max-height: 700px; + display: flex; + flex-direction: column; + overflow: hidden; } #xterm-terminal { - padding: 10px; + flex: 1; + width: 100%; + height: 100%; + overflow: hidden; + padding: 0; +} + +.xterm { + height: 100% !important; + width: 100% !important; +} + +.xterm-viewport { + height: 100% !important; } #terminal-status { diff --git a/src/public/js/terminal.js b/src/public/js/terminal.js index 92fa497..234fd48 100644 --- a/src/public/js/terminal.js +++ b/src/public/js/terminal.js @@ -2,6 +2,64 @@ let terminal = null; let socket = null; let isConnected = false; +let resizeObserver = null; + +// Initialize xterm with proper configuration and addons +function initializeTerminal() { + if (terminal) return; // Already initialized + + const xtermContainer = document.getElementById('xterm-terminal'); + + terminal = new Terminal({ + cursorBlink: true, + cursorStyle: 'block', + scrollback: 1000, + fontSize: 12, + fontFamily: 'Courier New, monospace', + theme: { + background: '#212529', + foreground: '#ffffff', + cursor: '#ffffff', + selection: 'rgba(255, 255, 255, 0.3)' + }, + allowTransparency: false, + rendererType: 'canvas', + lineHeight: 1.2 + }); + + terminal.open(xtermContainer); + + // Fit terminal to container dimensions + fitTerminal(); + + // Set up resize observer to fit terminal when container size changes + resizeObserver = new ResizeObserver(() => { + if (terminal) { + fitTerminal(); + } + }); + + resizeObserver.observe(xtermContainer); +} + +// Properly fit terminal to container +function fitTerminal() { + if (!terminal) return; + + const container = document.getElementById('xterm-terminal'); + if (!container) return; + + const { width, height } = container.getBoundingClientRect(); + const charWidth = terminal._core._charWidth || 7; + const charHeight = terminal._core._charHeight || 14; + + const cols = Math.max(80, Math.floor(width / charWidth) - 2); + const rows = Math.max(24, Math.floor(height / charHeight) - 2); + + if (terminal.cols !== cols || terminal.rows !== rows) { + terminal.resize(cols, rows); + } +} function connectTerminal(event) { event.preventDefault(); @@ -15,17 +73,9 @@ function connectTerminal(event) { // Show connecting status statusMessage.textContent = 'Connecting to terminal...'; - // Initialize xterm terminal - if (!terminal) { - terminal = new Terminal({ - cursorBlink: true, - theme: { - background: '#212529', - foreground: '#ffffff' - } - }); - terminal.open(document.getElementById('xterm-terminal')); - } + // Initialize xterm terminal if not already done + initializeTerminal(); + // Determine protocol const protocol = (location.protocol === 'https:') ? 'wss' : 'ws'; @@ -48,13 +98,6 @@ function connectTerminal(event) { // Send initial resize socket.send(JSON.stringify({ type: 'resize', cols: terminal.cols, rows: terminal.rows })); - - // Send the docker compose logs command after connection is established - setTimeout(() => { - terminal.write('\r\nExecuting: cd Console && docker compose logs\r\n'); - const command = "dir\n" ; 'cd Console && docker compose logs\r'; - // socket.send(command); - }, 1500); }; socket.onmessage = (event) => { @@ -103,7 +146,10 @@ function connectTerminal(event) { // Handle window resize window.addEventListener('resize', () => { - if (terminal && socket && socket.readyState === WebSocket.OPEN) { + if (terminal) { + fitTerminal(); + } + if (socket && socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify({ type: 'resize', cols: terminal.cols, rows: terminal.rows })); } }); @@ -123,7 +169,7 @@ function disconnectTerminal() { statusMessage.textContent = 'Terminal disconnected'; if (terminal) { - terminal.clear(); + terminal.reset(); } isConnected = false; @@ -146,6 +192,10 @@ function showTerminalAuth() { // Clean up when terminal app is closed function cleanupTerminal() { + if (resizeObserver) { + resizeObserver.disconnect(); + resizeObserver = null; + } if (socket) { socket.close(); } diff --git a/src/views/index.hbs b/src/views/index.hbs index c6ce685..0d80acf 100644 --- a/src/views/index.hbs +++ b/src/views/index.hbs @@ -208,8 +208,8 @@ {{! Terminal Container }} -