Skip to main content

Dashboard di monitoraggio

✅ Obiettivo

Creare una dashboard web browser-based con tema scuro, responsive e visualizzabile anche su smartphone, per monitorare un server Ubuntu via SSH. È una Progressive Web App (PWA) installabile su Android/iOS, con grafici live e aggiornamenti in tempo reale via Socket.io.


🧱 Stack Tecnologico

  • Node.js + Express: backend server

  • Socket.io: comunicazione real-time

  • Chart.js: grafici dinamici

  • HTML + CSS (dark mode): frontend

  • PWA: manifest.json, service worker, icone


📁 Struttura del progetto

pgsql
server-dashboard/ ├── public/ │ ├── index.html │ ├── style.css │ ├── script.js │ ├── manifest.json │ ├── service-worker.js │ └── icons/ │ ├── icon-96.png │ ├── icon-144.png │ ├── icon-192.png │ └── icon-512.png ├── server.js ├── package.json └── pm2-config.json (opzionale)

⚙️ Codice

📄 server.js (backend con Socket.io + Express)

js
const express = require('express'); const http = require('http'); const socketIo = require('socket.io'); const os = require('os'); const si = require('systeminformation'); const path = require('path'); const app = express(); const server = http.createServer(app); const io = socketIo(server); app.use(express.static(path.join(__dirname, 'public'))); io.on('connection', (socket) => { console.log('Client connesso'); const interval = setInterval(async () => { const cpu = await si.currentLoad(); const mem = await si.mem(); const disk = await si.fsSize(); const net = await si.networkStats(); const temp = await si.cpuTemperature(); const gpu = (await si.graphics()).controllers[0] || {}; socket.emit('stats', { cpu: cpu.currentLoad.toFixed(1), memUsed: (mem.used / 1024 / 1024 / 1024).toFixed(2), memTotal: (mem.total / 1024 / 1024 / 1024).toFixed(2), diskUsed: disk[0]?.used || 0, diskTotal: disk[0]?.size || 1, netRx: net[0]?.rx_sec / 1024 || 0, netTx: net[0]?.tx_sec / 1024 || 0, temp: temp.main || 0, gpuLoad: gpu.utilizationGpu || 0, gpuTemp: gpu.temperatureGpu || 0 }); }, 2000); socket.on('disconnect', () => { clearInterval(interval); console.log('Client disconnesso'); }); }); const PORT = 3000; server.listen(PORT, () => { console.log(`Dashboard disponibile su http://localhost:${PORT}`); });

📄 public/index.html (dashboard + PWA)

html
<!DOCTYPE html> <html lang="it"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>LoadingCT Server Dashboard</title> <link rel="stylesheet" href="style.css" /> <link rel="manifest" href="manifest.json" /> <meta name="theme-color" content="#ff6384" /> </head> <body> <h1>📊 LoadingCT Server Dashboard</h1> <table> <tr> <td><div class="info"><span class="icon">🖥️</span><span id="cpuText">CPU:</span></div><canvas id="chartCpu"></canvas></td> <td><div class="info"><span class="icon">💾</span><span id="ramText">RAM:</span></div><canvas id="chartRam"></canvas></td> <td><div class="info"><span class="icon">📊</span><span id="diskText">Disco:</span></div><canvas id="chartDisk"></canvas></td> </tr> <tr> <td><div class="info"><span class="icon">🌡️</span><span id="tempText">Temp:</span></div><canvas id="chartTemp"></canvas></td> <td><div class="info"><span class="icon">🌐</span><span id="netText">Rete:</span></div><canvas id="chartNet"></canvas></td> <td><div class="info"><span class="icon">🎮</span><span id="gpuText">GPU:</span></div><canvas id="chartGpu"></canvas></td> </tr> </table> <script src="/socket.io/socket.io.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="script.js"></script> <script> if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/service-worker.js') .then(() => console.log('Service Worker registrato')) .catch(err => console.error('Service Worker errore:', err)); } </script> </body> </html>

🎨 public/style.css (tema scuro)

css
body { background-color: #121212; color: #eee; font-family: Arial, sans-serif; margin: 20px; text-align: center; } h1 { margin-bottom: 20px; } table { width: 90vw; max-width: 800px; margin: 0 auto 30px auto; border-collapse: collapse; table-layout: fixed; color: #eee; } td { border: 1px solid #444; padding: 15px; vertical-align: top; background: #1f1f1f; text-align: center; } .info { display: flex; align-items: center; justify-content: center; gap: 8px; font-size: 1.2em; margin-bottom: 10px; } .icon { font-size: 1.6em; } canvas { max-width: 100%; height: 150px !important; margin: 0 auto; background-color: #222; border-radius: 8px; display: block; }

📈 public/script.js (client Socket.io + grafici)

js
document.addEventListener('DOMContentLoaded', () => { const socket = io(); const maxDataPoints = 30; function createChart(ctx, label, color, unit = '') { if (!ctx) return null; return new Chart(ctx, { type: 'line', data: { labels: Array(maxDataPoints).fill(''), datasets: [{ label: label, data: Array(maxDataPoints).fill(0), borderColor: color, backgroundColor: color + '44', fill: true, tension: 0.3, pointRadius: 0, }] }, options: { animation: false, responsive: true, maintainAspectRatio: false, scales: { x: { display: false }, y: { beginAtZero: true, ticks: { callback: val => val + unit } } }, plugins: { legend: { display: true }, } } }); } const charts = { cpu: createChart(document.getElementById('chartCpu')?.getContext('2d'), 'CPU %', '#ff6384', '%'), ram: createChart(document.getElementById('chartRam')?.getContext('2d'), 'RAM GB', '#36a2eb', 'GB'), disk: createChart(document.getElementById('chartDisk')?.getContext('2d'), 'Disco GB usati', '#ffce56', 'GB'), temp: createChart(document.getElementById('chartTemp')?.getContext('2d'), 'Temperatura °C', '#ff9f40', '°C'), net: createChart(document.getElementById('chartNet')?.getContext('2d'), 'Rete KB/s', '#4bc0c0', 'KB/s'), gpu: createChart(document.getElementById('chartGpu')?.getContext('2d'), 'GPU %', '#9966ff', '%'), }; function updateChart(chart, value) { if (!chart) return; chart.data.datasets[0].data.push(value); if (chart.data.datasets[0].data.length > maxDataPoints) { chart.data.datasets[0].data.shift(); } chart.update('none'); } socket.on('stats', (data) => { document.getElementById('cpuText').textContent = `CPU: ${data.cpu}%`; document.getElementById('ramText').textContent = `RAM: ${data.memUsed} / ${data.memTotal} GB`; document.getElementById('diskText').textContent = `Disco: ${(data.diskUsed / 1024 / 1024 / 1024).toFixed(2)} / ${(data.diskTotal / 1024 / 1024 / 1024).toFixed(2)} GB`; document.getElementById('tempText').textContent = `Temp CPU: ${data.temp}°C`; document.getElementById('netText').textContent = `Rete: ↓ ${data.netRx.toFixed(1)} KB/s ↑ ${data.netTx.toFixed(1)} KB/s`; document.getElementById('gpuText').textContent = `GPU: ${data.gpuLoad}% - ${data.gpuTemp}°C`; updateChart(charts.cpu, data.cpu); updateChart(charts.ram, data.memUsed); updateChart(charts.disk, (data.diskUsed / 1024 / 1024 / 1024).toFixed(2)); updateChart(charts.temp, data.temp); updateChart(charts.net, (data.netRx + data.netTx).toFixed(1)); updateChart(charts.gpu, data.gpuLoad); }); });

🌐 public/manifest.json

json
{ "name": "LoadingCT_Dashboard", "short_name": "LoadingCT", "description": "Dashboard di monitoraggio server LoadingCT", "start_url": "/index.html", "display": "standalone", "background_color": "#121212", "theme_color": "#ff6384", "orientation": "portrait", "icons": [ { "src": "icons/icon-96.png", "sizes": "96x96", "type": "image/png" }, { "src": "icons/icon-144.png", "sizes": "144x144", "type": "image/png" }, { "src": "icons/icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "icons/icon-512.png", "sizes": "512x512", "type": "image/png" } ] }

🛠️ public/service-worker.js

js
self.addEventListener('install', e => { e.waitUntil( caches.open('dashboard-cache').then(cache => cache.addAll([ '/', '/index.html', '/style.css', '/script.js', '/manifest.json', 'https://cdn.jsdelivr.net/npm/chart.js', '/socket.io/socket.io.js' ]) ) ); }); self.addEventListener('fetch', e => { e.respondWith( caches.match(e.request).then(response => response || fetch(e.request)) ); });

🖼️ Icone (cartella public/icons)

Puoi generare le icone a partire da una immagine base e ridimensionarle in:

  • 96x96, 144x144, 192x192, 512x512 (PNG)