diff --git a/src/controllers/admin.controller.js b/src/controllers/admin.controller.js index 29b6f4b..a53dc05 100644 --- a/src/controllers/admin.controller.js +++ b/src/controllers/admin.controller.js @@ -1,4 +1,5 @@ import * as adminService from '../services/admin.service.js'; +import * as statusService from '../services/status.service.js'; import { logUserCreation, logRoleChange } from '../services/audit.service.js'; /** @@ -142,3 +143,20 @@ export const deleteUser = async (req, res, next) => { return next(error); } }; + +/** + * Get server status (admin only) + * GET /api/admin/server-status + */ +export const getServerStatus = async (req, res, next) => { + try { + const status = await statusService.getServerStatus(); + + res.status(200).json({ + success: true, + data: status, + }); + } catch (error) { + return next(error); + } +}; diff --git a/src/routes/admin.routes.js b/src/routes/admin.routes.js index 9181b09..cdb1903 100644 --- a/src/routes/admin.routes.js +++ b/src/routes/admin.routes.js @@ -50,4 +50,11 @@ router.delete( adminController.deleteUser ); +/** + * Server status + */ + +// Server status (CPU, RAM, Disk, Network, Uptime) +router.get('/server-status', adminController.getServerStatus); + export default router; diff --git a/src/services/status.service.js b/src/services/status.service.js new file mode 100644 index 0000000..a065e9d --- /dev/null +++ b/src/services/status.service.js @@ -0,0 +1,186 @@ +import os from 'os'; +import path from 'path'; +import { execSync } from 'child_process'; +import { pool } from '../config/database.js'; + +/** + * Get CPU usage percentage + */ +const getCpuUsage = () => { + const cpus = os.cpus(); + let totalIdle = 0; + let totalTick = 0; + + cpus.forEach((cpu) => { + for (const type in cpu.times) { + totalTick += cpu.times[type]; + } + totalIdle += cpu.times.idle; + }); + + const idle = totalIdle / cpus.length; + const total = totalTick / cpus.length; + const percent = Math.round(((total - idle) / total) * 100); + + return { percent }; +}; + +/** + * Get memory usage (Linux - reads /proc/meminfo for accurate values) + */ +const getMemoryUsage = () => { + try { + // Read /proc/meminfo for accurate memory stats (like htop) + const meminfo = execSync('cat /proc/meminfo', { encoding: 'utf8' }); + const lines = meminfo.split('\n'); + + const getValue = (key) => { + const line = lines.find(l => l.startsWith(key)); + if (!line) return 0; + const match = line.match(/(\d+)/); + return match ? parseInt(match[1]) : 0; + }; + + const totalKB = getValue('MemTotal'); + const freeKB = getValue('MemFree'); + const buffersKB = getValue('Buffers'); + const cachedKB = getValue('Cached'); + const sreclaimableKB = getValue('SReclaimable'); + + // Available = Free + Buffers + Cached + SReclaimable (like htop calculates) + const availableKB = freeKB + buffersKB + cachedKB + sreclaimableKB; + const usedKB = totalKB - availableKB; + + // Convert to GB + const totalGB = Math.round((totalKB / (1024 ** 2)) * 10) / 10; + const usedGB = Math.round((usedKB / (1024 ** 2)) * 10) / 10; + + return { + used: usedGB, + total: totalGB, + unit: 'GB', + }; + } catch { + // Fallback to os module + const totalBytes = os.totalmem(); + const freeBytes = os.freemem(); + const usedBytes = totalBytes - freeBytes; + + return { + used: Math.round((usedBytes / (1024 ** 3)) * 10) / 10, + total: Math.round((totalBytes / (1024 ** 3)) * 10) / 10, + unit: 'GB', + }; + } +}; + +/** + * Get disk usage (Linux/macOS) + */ +const getDiskUsage = () => { + try { + const output = execSync('df -BG / | tail -1', { encoding: 'utf8' }); + const parts = output.trim().split(/\s+/); + + const totalGB = parseInt(parts[1]) || 0; + const usedGB = parseInt(parts[2]) || 0; + + return { + used: usedGB, + total: totalGB, + unit: 'GB', + }; + } catch { + return { used: 0, total: 0, unit: 'GB' }; + } +}; + +/** + * Get Node.js backend process stats (CPU + RAM) + */ +const getBackendStats = () => { + const memUsage = process.memoryUsage(); + + // Get CPU usage for this process + const cpuUsage = process.cpuUsage(); + const cpuPercent = Math.round(((cpuUsage.user + cpuUsage.system) / 1000000) * 100) / 100; + + return { + memoryMB: Math.round((memUsage.rss / (1024 ** 2)) * 10) / 10, + heapMB: Math.round((memUsage.heapUsed / (1024 ** 2)) * 10) / 10, + cpuTime: cpuPercent, // CPU time in seconds + }; +}; + +/** + * Get uploads directory size + */ +const getUploadsSize = () => { + try { + const uploadsPath = path.join(process.cwd(), 'uploads'); + const output = execSync(`du -sm "${uploadsPath}" 2>/dev/null || echo "0"`, { encoding: 'utf8' }); + const sizeMB = parseInt(output.split('\t')[0]) || 0; + + return { + sizeMB, + unit: 'MB', + }; + } catch { + return { sizeMB: 0, unit: 'MB' }; + } +}; + +/** + * Get database table count + */ +const getTableCount = async () => { + try { + const result = await pool.query(` + SELECT COUNT(*) as count + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_type = 'BASE TABLE' + `); + return parseInt(result.rows[0]?.count) || 0; + } catch { + return 0; + } +}; + +/** + * Get system uptime in seconds + */ +const getSystemUptime = () => { + return Math.floor(os.uptime()); +}; + +/** + * Get Node.js process uptime in seconds + */ +const getProcessUptime = () => { + return Math.floor(process.uptime()); +}; + +/** + * Get complete server status + */ +export const getServerStatus = async () => { + const tableCount = await getTableCount(); + + return { + cpu: getCpuUsage(), + memory: getMemoryUsage(), + disk: getDiskUsage(), + backend: getBackendStats(), + uploads: getUploadsSize(), + database: { + tableCount, + }, + systemUptimeSec: getSystemUptime(), + backendUptimeSec: getProcessUptime(), + hostname: os.hostname(), + platform: os.platform(), + nodeVersion: process.version, + timestamp: new Date().toISOString(), + }; +};