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-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..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", @@ -24,7 +23,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 +34,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..92fa497 --- /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