From 1d609aaf7f5ff75418980585ca8c24adcde6ebb9 Mon Sep 17 00:00:00 2001 From: Araan-Sheikh Date: Fri, 24 Oct 2025 22:34:22 +0530 Subject: [PATCH 1/2] feat: Add real-time server dashboard and stats service - Updated package.json to include new dependencies: axios, chart.js, socket.io, and ws. - Modified pages.json to add a new route for the real-time dashboard. - Implemented a new realtime.js API for handling socket connections and server subscriptions. - Created StatsService.js to manage polling and broadcasting server statistics. - Developed realtime-dashboard.ejs for the front-end interface, including charts and console output. --- api/realtime.js | 198 ++++++++++++++ index.js | 21 +- managers/StatsService.js | 140 ++++++++++ package-lock.json | 299 ++++++++++++++++++++- package.json | 7 +- themes/lapsus/pages.json | 6 +- themes/lapsus/realtime-dashboard.ejs | 374 +++++++++++++++++++++++++++ 7 files changed, 1030 insertions(+), 15 deletions(-) create mode 100644 api/realtime.js create mode 100644 managers/StatsService.js create mode 100644 themes/lapsus/realtime-dashboard.ejs diff --git a/api/realtime.js b/api/realtime.js new file mode 100644 index 0000000..f47bb00 --- /dev/null +++ b/api/realtime.js @@ -0,0 +1,198 @@ +const settings = require("../settings.json"); +const chalk = require('chalk'); +const fetch = require('node-fetch'); + +module.exports.load = async function(app, db) { + const indexjs = require("../index.js"); + const io = indexjs.io; + const statsService = indexjs.statsService; + + if (!io || !statsService) { + console.error(chalk.red('[REALTIME] Socket.io or StatsService not initialized')); + return; + } + io.use(async (socket, next) => { + const sessionId = socket.handshake.auth.sessionId; + const userId = socket.handshake.auth.userId; + if (!userId || !sessionId) { + return next(new Error('Authentication required')); + } + const userExists = await db.get(`users-${userId}`); + if (!userExists) { + return next(new Error('Invalid user')); + } + + socket.userId = userId; + socket.pteroUserId = userExists; + next(); + }); + io.on('connection', (socket) => { + console.log(chalk.cyan(`[REALTIME] User ${socket.userId} connected`)); + socket.join(`user-${socket.userId}`); + socket.on('subscribe-dashboard', async () => { + socket.join('dashboard'); + const allServers = statsService.getAllServersSnapshot(); + socket.emit('dashboard-init', allServers); + + console.log(chalk.green(`[REALTIME] User ${socket.userId} subscribed to dashboard`)); + }); + socket.on('subscribe-server', async (data) => { + const { identifier } = data; + + if (!identifier) { + socket.emit('error', { message: 'Server identifier required' }); + return; + } + const hasAccess = await verifyServerAccess(socket.pteroUserId, identifier); + if (!hasAccess) { + socket.emit('error', { message: 'Access denied' }); + return; + } + socket.join(`server-${identifier}`); + const serverData = statsService.getServerStatsSnapshot(identifier); + if (serverData) { + socket.emit('server-init', serverData); + } + + console.log(chalk.green(`[REALTIME] User ${socket.userId} subscribed to server ${identifier}`)); + }); + socket.on('subscribe-console', async (data) => { + const { identifier } = data; + + if (!identifier) { + socket.emit('error', { message: 'Server identifier required' }); + return; + } + const hasAccess = await verifyServerAccess(socket.pteroUserId, identifier); + if (!hasAccess) { + socket.emit('error', { message: 'Access denied' }); + return; + } + const wsUrl = await getServerWebSocketUrl(identifier); + if (wsUrl) { + connectToServerConsole(socket, identifier, wsUrl); + } else { + socket.emit('error', { message: 'Could not connect to server console' }); + } + }); + socket.on('unsubscribe-dashboard', () => { + socket.leave('dashboard'); + console.log(chalk.yellow(`[REALTIME] User ${socket.userId} unsubscribed from dashboard`)); + }); + + socket.on('unsubscribe-server', (data) => { + const { identifier } = data; + socket.leave(`server-${identifier}`); + console.log(chalk.yellow(`[REALTIME] User ${socket.userId} unsubscribed from server ${identifier}`)); + }); + socket.on('disconnect', () => { + console.log(chalk.yellow(`[REALTIME] User ${socket.userId} disconnected`)); + }); + socket.on('request-stats-history', async (data) => { + const { identifier, duration } = data; + + const hasAccess = await verifyServerAccess(socket.pteroUserId, identifier); + if (!hasAccess) { + socket.emit('error', { message: 'Access denied' }); + return; + } + + const history = await statsService.getServerHistory(identifier, duration); + socket.emit('stats-history', { identifier, history }); + }); + }); + async function verifyServerAccess(pteroUserId, identifier) { + try { + const response = await fetch( + `${settings.pterodactyl.domain}/api/client/servers/${identifier}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${settings.pterodactyl.account_key}`, + 'Accept': 'application/json' + } + } + ); + + return response.ok; + } catch (error) { + return false; + } + } + async function getServerWebSocketUrl(identifier) { + try { + const response = await fetch( + `${settings.pterodactyl.domain}/api/client/servers/${identifier}/websocket`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${settings.pterodactyl.account_key}`, + 'Accept': 'application/json' + } + } + ); + + if (!response.ok) return null; + + const data = await response.json(); + return data.data; + } catch (error) { + return null; + } + } + function connectToServerConsole(clientSocket, identifier, wsData) { + const WebSocket = require('ws'); + const ws = new WebSocket(wsData.socket); + + ws.on('open', () => { + ws.send(JSON.stringify({ + event: 'auth', + args: [wsData.token] + })); + ws.send(JSON.stringify({ + event: 'send logs', + args: [null] + })); + + console.log(chalk.green(`[REALTIME] Console connected for server ${identifier}`)); + }); + + ws.on('message', (data) => { + try { + const message = JSON.parse(data); + if (message.event === 'console output') { + clientSocket.emit('console-output', { + identifier, + output: message.args[0] + }); + } + if (message.event === 'status') { + clientSocket.emit('console-status', { + identifier, + status: message.args[0] + }); + } + } catch (error) { + console.error(chalk.red('[REALTIME] Error parsing console message:'), error); + } + }); + + ws.on('error', (error) => { + console.error(chalk.red(`[REALTIME] Console WebSocket error for ${identifier}:`), error); + clientSocket.emit('console-error', { + identifier, + message: 'Console connection error' + }); + }); + ws.on('close', () => { + console.log(chalk.yellow(`[REALTIME] Console disconnected for server ${identifier}`)); + clientSocket.emit('console-disconnected', { identifier }); + }); + clientSocket.on('disconnect', () => { + ws.close(); + }); + clientSocket.consoleWs = ws; + } +}; diff --git a/index.js b/index.js index 45359ca..0a8170c 100644 --- a/index.js +++ b/index.js @@ -118,7 +118,22 @@ app.use(express.json({ verify: undefined })); -const listener = app.listen(settings.website.port, function() { +const http = require('http'); +const server = http.createServer(app); +const { Server } = require('socket.io'); +const io = new Server(server, { + cors: { + origin: settings.api.client.oauth2.link, + credentials: true + } +}); + +const StatsService = require('./managers/StatsService'); +const statsService = new StatsService(settings, db); +module.exports.io = io; +module.exports.statsService = statsService; + +const listener = server.listen(settings.website.port, function() { console.clear(); console.log(chalk.gray(" ")); console.log(chalk.gray(" ") + chalk.bgBlack(" LAPSUS CLIENT IS ONLINE ")); @@ -128,6 +143,10 @@ const listener = app.listen(settings.website.port, function() { console.log(chalk.gray(" ") + chalk.blue("[THEME]") + chalk.white(" You're using ") + chalk.underline(settings.defaulttheme) + " theme"); console.log(chalk.gray(" ")); console.log(chalk.gray(" ") + chalk.cyan("[SYSTEM]") + chalk.white(" You can now access the dashboard at ") + chalk.underline(settings.api.client.oauth2.link + "/")); + console.log(chalk.gray(" ")); + console.log(chalk.gray(" ") + chalk.magenta("[REALTIME]") + chalk.white(" WebSocket server initialized for live stats")); + statsService.initialize(io); + if (settings.defaulttheme !== 'lapsus' && settings.defaulttheme !== 'lapsusv2' && settings.defaulttheme !== 'pylex') { console.log(chalk.gray(" ")); console.log(chalk.gray(" ") + chalk.yellow("[WARNING]") + chalk.white(" You're using an unofficial theme. This means you are exposed to vulnerabilities and bugs. Consider using the official theme or a third party theme provided by Lapsus.")); } diff --git a/managers/StatsService.js b/managers/StatsService.js new file mode 100644 index 0000000..4fe5656 --- /dev/null +++ b/managers/StatsService.js @@ -0,0 +1,140 @@ +const fetch = require('node-fetch'); +const chalk = require('chalk'); + +class StatsService { + constructor(settings, db) { + this.settings = settings; + this.db = db; + this.io = null; + this.updateInterval = 5000; + this.serverStats = new Map(); + this.intervalId = null; + } + + initialize(io) { + this.io = io; + console.log(chalk.green('[STATS] Real-time stats service initialized')); + this.startPolling(); + } + + startPolling() { + if (this.intervalId) return; + this.intervalId = setInterval(async () => { + await this.pollAllServers(); + }, this.updateInterval); + this.pollAllServers(); + } + + stopPolling() { + if (this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = null; + } + } + + async pollAllServers() { + try { + const userKeys = await this.getAllUserKeys(); + for (const userKey of userKeys) { + const userId = userKey.replace('users-', ''); + const pteroUserId = await this.db.get(userKey); + if (!pteroUserId) continue; + const servers = await this.getUserServers(pteroUserId); + + if (!servers) continue; + + for (const server of servers) { + const stats = await this.getServerStats(server.attributes.identifier); + + if (stats) { + const serverData = { + serverId: server.attributes.id, + identifier: server.attributes.identifier, + name: server.attributes.name, + userId: userId, + status: stats.current_state || 'offline', + resources: stats.resources || {}, + timestamp: Date.now() + }; + + this.serverStats.set(server.attributes.identifier, serverData); + this.io.to(`server-${server.attributes.identifier}`).emit('server-stats', serverData); + this.io.to('dashboard').emit('server-update', serverData); + } + } + } + } catch (error) { + console.error(chalk.red('[STATS] Error polling servers:'), error.message); + } + } + + async getAllUserKeys() { + try { + const keys = []; + return keys; + } catch (error) { + return []; + } + } + + async getUserServers(pteroUserId) { + try { + const response = await fetch( + `${this.settings.pterodactyl.domain}/api/application/users/${pteroUserId}?include=servers`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.settings.pterodactyl.key}`, + 'Accept': 'application/json' + } + } + ); + + if (!response.ok) return null; + + const data = await response.json(); + return data.attributes?.relationships?.servers?.data || []; + } catch (error) { + console.error(chalk.red('[STATS] Error fetching user servers:'), error.message); + return null; + } + } + + async getServerStats(identifier) { + try { + const response = await fetch( + `${this.settings.pterodactyl.domain}/api/client/servers/${identifier}/resources`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.settings.pterodactyl.account_key}`, + 'Accept': 'application/json' + } + } + ); + + if (!response.ok) return null; + + const data = await response.json(); + return data.attributes || null; + } catch (error) { + return null; + } + } + + getServerStatsSnapshot(identifier) { + return this.serverStats.get(identifier); + } + + getAllServersSnapshot() { + return Array.from(this.serverStats.values()); + } + + async getServerHistory(identifier, duration = 3600000) { // 1 hour default + return [this.serverStats.get(identifier)]; + } +} + +module.exports = StatsService; diff --git a/package-lock.json b/package-lock.json index 4ef5fed..90fdeea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,9 +15,10 @@ "@keyv/redis": "^2.2.1", "@keyv/sqlite": "^3.5.2", "@tailwindcss/forms": "^0.5.3", - "axios": "^1.9.0", + "axios": "^1.12.2", "body-parser": "^1.20.3", "chalk": "^4.1.0", + "chart.js": "^4.5.1", "cookie-session": "^2.1.0", "cors": "^2.8.5", "credit-card-validate": "^0.9.3", @@ -41,11 +42,13 @@ "passport-google-oauth20": "^2.0.0", "path": "^0.12.7", "smtp-server": "^3.13.5", + "socket.io": "^4.8.1", "stripe": "^9.4.0", "systeminformation": "^5.25.11", "tailwindcss": "^3.3.1", "unzipper": "^0.12.3", - "warn": "^1.0.1" + "warn": "^1.0.1", + "ws": "^8.18.3" } }, "node_modules/@alloc/quick-lru": { @@ -302,6 +305,12 @@ "node": ">= 14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@mongodb-js/saslprep": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.0.tgz", @@ -408,6 +417,12 @@ "url": "https://ko-fi.com/killymxi" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@tailwindcss/forms": { "version": "0.5.10", "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz", @@ -430,6 +445,15 @@ "node": ">= 6" } }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", @@ -742,9 +766,9 @@ } }, "node_modules/axios": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz", - "integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -787,6 +811,15 @@ ], "license": "MIT" }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/base64url": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", @@ -1079,6 +1112,18 @@ "node": "*" } }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -1522,6 +1567,27 @@ "node": ">=12.0.0" } }, + "node_modules/discord.js/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/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -1731,6 +1797,88 @@ "once": "^1.4.0" } }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "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/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -5486,6 +5634,137 @@ "node": ">=6.0.0" } }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "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/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/socks": { "version": "2.8.7", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", @@ -6467,16 +6746,16 @@ "license": "ISC" }, "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 8318f00..4f05afd 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,10 @@ "@keyv/redis": "^2.2.1", "@keyv/sqlite": "^3.5.2", "@tailwindcss/forms": "^0.5.3", - "axios": "^1.9.0", + "axios": "^1.12.2", "body-parser": "^1.20.3", "chalk": "^4.1.0", + "chart.js": "^4.5.1", "cookie-session": "^2.1.0", "cors": "^2.8.5", "credit-card-validate": "^0.9.3", @@ -38,11 +39,13 @@ "passport-google-oauth20": "^2.0.0", "path": "^0.12.7", "smtp-server": "^3.13.5", + "socket.io": "^4.8.1", "stripe": "^9.4.0", "systeminformation": "^5.25.11", "tailwindcss": "^3.3.1", "unzipper": "^0.12.3", - "warn": "^1.0.1" + "warn": "^1.0.1", + "ws": "^8.18.3" }, "scripts": { "start": "nodemon index.js", diff --git a/themes/lapsus/pages.json b/themes/lapsus/pages.json index 8ae848e..3e1a7af 100644 --- a/themes/lapsus/pages.json +++ b/themes/lapsus/pages.json @@ -69,7 +69,8 @@ "servers": "servers.ejs", "lv": "lv.ejs", "j4r": "j4r.ejs", - "themes": "themes.ejs" + "themes": "themes.ejs", + "realtime": "realtime-dashboard.ejs" }, "mustbeloggedin": [ "/dashboard", @@ -82,7 +83,8 @@ "/redeem", "/servers", "/store", - "/earn" + "/earn", + "/realtime" ], "mustbeadmin": [ "/admin", diff --git a/themes/lapsus/realtime-dashboard.ejs b/themes/lapsus/realtime-dashboard.ejs new file mode 100644 index 0000000..0b97b20 --- /dev/null +++ b/themes/lapsus/realtime-dashboard.ejs @@ -0,0 +1,374 @@ + + + + + + Real-Time Server Dashboard - <%= settings.name %> + + + + + + + + +
+
+
+ +
+

Real-Time Server Dashboard

+

Live monitoring of all your servers

+
+
+
+
+ Connection Status: + + Connected + +
+ + Back to Dashboard + +
+
+
+ + +
+
+
+
+

Total Servers

+

0

+
+ +
+
+
+
+
+

Online

+

0

+
+ +
+
+
+
+
+

Offline

+

0

+
+ +
+
+
+
+
+

Avg. CPU Usage

+

0%

+
+ +
+
+
+
+
+ + + + + From 47c0419e6dba1d95fabf4314d9b4f1dc78bd5fc3 Mon Sep 17 00:00:00 2001 From: Araan-Sheikh Date: Sat, 25 Oct 2025 07:57:25 +0530 Subject: [PATCH 2/2] feat: Implement real-time server monitoring dashboard with updated stats fetching. --- managers/StatsService.js | 62 +- themes/lapsus/components/navigation.ejs | 7 + themes/lapsus/realtime-dashboard.ejs | 737 +++++++++++++----------- 3 files changed, 440 insertions(+), 366 deletions(-) diff --git a/managers/StatsService.js b/managers/StatsService.js index 4fe5656..937cb15 100644 --- a/managers/StatsService.js +++ b/managers/StatsService.js @@ -34,34 +34,44 @@ class StatsService { async pollAllServers() { try { - const userKeys = await this.getAllUserKeys(); - for (const userKey of userKeys) { - const userId = userKey.replace('users-', ''); - const pteroUserId = await this.db.get(userKey); - if (!pteroUserId) continue; - const servers = await this.getUserServers(pteroUserId); - - if (!servers) continue; - - for (const server of servers) { - const stats = await this.getServerStats(server.attributes.identifier); - - if (stats) { - const serverData = { - serverId: server.attributes.id, - identifier: server.attributes.identifier, - name: server.attributes.name, - userId: userId, - status: stats.current_state || 'offline', - resources: stats.resources || {}, - timestamp: Date.now() - }; - - this.serverStats.set(server.attributes.identifier, serverData); - this.io.to(`server-${server.attributes.identifier}`).emit('server-stats', serverData); - this.io.to('dashboard').emit('server-update', serverData); + const allServersResponse = await fetch( + `${this.settings.pterodactyl.domain}/api/application/servers?per_page=100`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.settings.pterodactyl.key}`, + 'Accept': 'application/json' } } + ); + + if (!allServersResponse.ok) { + console.error(chalk.red('[STATS] Failed to fetch servers from Pterodactyl')); + return; + } + + const serversData = await allServersResponse.json(); + const servers = serversData.data || []; + + for (const server of servers) { + const stats = await this.getServerStats(server.attributes.identifier); + + if (stats) { + const serverData = { + serverId: server.attributes.id, + identifier: server.attributes.identifier, + name: server.attributes.name, + userId: server.attributes.user, + status: stats.current_state || 'offline', + resources: stats.resources || {}, + timestamp: Date.now() + }; + + this.serverStats.set(server.attributes.identifier, serverData); + this.io.to(`server-${server.attributes.identifier}`).emit('server-stats', serverData); + this.io.to('dashboard').emit('server-update', serverData); + } } } catch (error) { console.error(chalk.red('[STATS] Error polling servers:'), error.message); diff --git a/themes/lapsus/components/navigation.ejs b/themes/lapsus/components/navigation.ejs index 9510602..9f4c6c9 100644 --- a/themes/lapsus/components/navigation.ejs +++ b/themes/lapsus/components/navigation.ejs @@ -81,6 +81,13 @@ font-weight: semibold; Panel + + + + + Real-Time Monitor + +
diff --git a/themes/lapsus/realtime-dashboard.ejs b/themes/lapsus/realtime-dashboard.ejs index 0b97b20..3893f39 100644 --- a/themes/lapsus/realtime-dashboard.ejs +++ b/themes/lapsus/realtime-dashboard.ejs @@ -1,374 +1,431 @@ - - - - - Real-Time Server Dashboard - <%= settings.name %> - - - - - + Real-Time Dashboard - <%= settings.name %> + + + + + + + + + + + - - -
-
-
- -
-

Real-Time Server Dashboard

-

Live monitoring of all your servers

-
+ + + + <%- include('components/navigation') %> + +
+
+
+
+ + +
+
+

+ Real-Time Server Monitor +

+

+ Live statistics and console streaming for all your servers +

-
-
- Connection Status: - - Connected - -
- - Back to Dashboard - +
+ + Connection: + ● Connected +
-
-
+
- -
-
-
-
-

Total Servers

-

0

+ +
+
+
+
+
+ + + +
+
+
+
Total Servers
+
0
+
+
- +
-
-
-
-
-

Online

-

0

+ +
+
+
+
+ + + +
+
+
+
Online
+
0
+
+
- +
-
-
-
-
-

Offline

-

0

+ +
+
+
+
+ + + +
+
+
+
Offline
+
0
+
+
- +
-
-
-
-
-

Avg. CPU Usage

-

0%

+ +
+
+
+
+ + + +
+
+
+
Avg CPU
+
0%
+
+
- +
+
+
+
+ +
-
-
-
-
+
- -
-
-

Live Console

- -
-
-
Connecting to server console...
-
-
-
-

- - 0 viewers watching this server -

-
+ - + networkChart = new Chart(document.getElementById('network-chart'), { + type: 'line', + data: { + labels: ['Now'], + datasets: [{ data: [0], borderColor: 'rgb(34, 197, 94)', backgroundColor: 'rgba(34, 197, 94, 0.1)', tension: 0.4 }] + }, + options: chartOptions + }); + } + function destroyCharts() { + if (cpuChart) cpuChart.destroy(); + if (memoryChart) memoryChart.destroy(); + if (networkChart) networkChart.destroy(); + } + socket.on('console-output', (data) => { + if (currentServer && data.identifier === currentServer.identifier) { + const output = document.getElementById('console-output'); + const line = document.createElement('div'); + line.className = 'console-line'; + line.textContent = data.output; + output.appendChild(line); + output.scrollTop = output.scrollHeight; + } + }); + function clearConsole() { + document.getElementById('console-output').innerHTML = ''; + } + function getStatusColor(status) { + const colors = { + 'running': 'bg-green-100 text-green-800', + 'offline': 'bg-red-100 text-red-800', + 'starting': 'bg-yellow-100 text-yellow-800', + 'stopping': 'bg-orange-100 text-orange-800' + }; + return colors[status] || 'bg-gray-100 text-gray-800'; + } + function formatBytes(bytes) { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; + } + -