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)


 

Update

🔧 1. Backend – server.js

Aggiungiamo l’utilizzo di si.fsSize() per ottenere tutti i filesystem montati e separatamente un controllo dedicato per il mount-point /mnt/storagepool.

js
// Usa questo nel tuo interval di stats: const fsArray = await si.fsSize(); // Statistiche disco singolo fsArray.forEach(d => { socket.emit('diskSingle', { fs: d.fs, mount: d.mount, usedGB: (d.used / 1024 / 1024 / 1024).toFixed(2), sizeGB: (d.size / 1024 / 1024 / 1024).toFixed(2), useP: d.use.toFixed(1) }); }); // Statistiche aggregate per lo storage pool const pool = fsArray.find(d => d.mount === '/mnt/storagepool'); if (pool) { socket.emit('diskPool', { usedGB: (pool.used / 1024 / 1024 / 1024).toFixed(2), sizeGB: (pool.size / 1024 / 1024 / 1024).toFixed(2), freeGB: ((pool.size - pool.used) / 1024 / 1024 / 1024).toFixed(2), useP: pool.use.toFixed(1) }); }

👉 si.fsSize() restituisce un array con tutti i filesystem montati, inclusi /, /boot, /mnt/... e così via github.com+2spec.org+2stackoverflow.com+2stackoverflow.com+6app.unpkg.com+6classic.yarnpkg.com+6.


📊 2. Frontend – script.js

Includi nuovi listener e visualizzazione:

js
socket.on('diskSingle', d => { // crea o aggiorna dinamicamente una lista riga/tabella div con id = 'disk-' + mount name // ad esempio: const id = 'disk-' + d.mount.replace(/[\/]/g, '_'); let el = document.getElementById(id); if (!el) { el = document.createElement('div'); el.id = id; el.innerHTML = `<strong>${d.mount}</strong>: ${d.usedGB}/${d.sizeGB} GB (${d.useP}%)`; document.getElementById('storage-details').appendChild(el); } else { el.textContent = `${d.mount}: ${d.usedGB}/${d.sizeGB} GB (${d.useP}%)`; } }); socket.on('diskPool', p => { const el = document.getElementById('pool-info'); el.textContent = `Storage Pool: ${p.usedGB}/${p.sizeGB} GB usati, ${p.freeGB} GB liberi (${p.useP}%)`; });

🧩 3. HTML – inserisci i contenitori nella pagina

Aggiungi sotto la tabella principale:

html
<div id="storage-section"> <h2>🗄️ Storage</h2> <div id="pool-info">Caricamento...</div> <div id="storage-details"></div> </div>

E nello style.css aggiungi un po' di stile per chiarezza:

css
#storage-section { width: 90vw; max-width: 800px; margin: 20px auto; background: #1f1f1f; padding: 15px; border-radius: 8px; color: #ccc; } #storage-section div { padding: 5px 0; font-size: 0.9em; }

  • si.fsSize() restituisce tutti i filesystem montati npmjs.com.

  • Nel backend invii due eventi via Socket:

    • diskSingle → ogni singolo disco/partizione.

    • diskPool → solo /mnt/storagepool per riassunto pool.

  • Nel frontend, aggiungi un’area dedicata in cui inserire queste info in tempo reale.

  • Nessun plugin aggiuntivo; tutto gestito con systeminformation e socket.io già in uso.

 

Versione 2.0

Dashboard di monitoraggio server Ubuntu via SSH


✅ 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 tra server e client

  • Chart.js: grafici dinamici e animati

  • HTML + CSS (dark mode): frontend e interfaccia utente

  • PWA: manifest.json, service worker e icone per installabilità


📁 Struttura del progetto

plaintext
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 + systeminformation)

js
// Import moduli necessari const express = require('express'); const http = require('http'); const socketIo = require('socket.io'); 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'); // Invio dati ogni 2 secondi const interval = setInterval(async () => { try { // Ottieni statistiche di sistema in parallelo const [cpu, mem, disks, net, temp, gpuRaw] = await Promise.all([ si.currentLoad(), si.mem(), si.fsSize(), si.networkStats(), si.cpuTemperature(), si.graphics() ]); const gpu = gpuRaw.controllers[0] || {}; // Invia dati di base per grafici socket.emit('stats', { cpu: cpu.currentLoad.toFixed(1), // CPU % memUsed: (mem.used / 1024 / 1024 / 1024).toFixed(2), // RAM usata GB memTotal: (mem.total / 1024 / 1024 / 1024).toFixed(2), // RAM totale GB diskUsed: disks[0]?.used || 0, // Primo disco usato (bytes) diskTotal: disks[0]?.size || 1, // Primo disco totale (bytes) temp: temp.main || 0, // Temperatura CPU (°C) netRx: (net[0]?.rx_sec || 0) / 1024, // Download KB/s netTx: (net[0]?.tx_sec || 0) / 1024, // Upload KB/s gpuLoad: gpu.utilizationGpu || 0, // Carico GPU % gpuTemp: gpu.temperatureGpu || 0 // Temperatura GPU °C }); // Statistiche disco singolo: invia info per ogni disco montato disks.forEach(d => { socket.emit('diskSingle', { fs: d.fs, mount: d.mount, usedGB: (d.used / 1024 / 1024 / 1024).toFixed(2), sizeGB: (d.size / 1024 / 1024 / 1024).toFixed(2), useP: d.use.toFixed(1) }); }); // Statistiche aggregate per lo storage pool (es. /mnt/storagepool) const pool = disks.find(d => d.mount === '/mnt/storagepool'); if (pool) { socket.emit('diskPool', { usedGB: (pool.used / 1024 / 1024 / 1024).toFixed(2), sizeGB: (pool.size / 1024 / 1024 / 1024).toFixed(2), freeGB: ((pool.size - pool.used) / 1024 / 1024 / 1024).toFixed(2), useP: pool.use.toFixed(1) }); } } catch (err) { console.error('Errore nel recupero stats:', err.message); } }, 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> <!-- Sezione Storage --> <div id="storage-section"> <h2>🗄️ Storage</h2> <div id="pool-info">Caricamento...</div> <div id="storage-details"></div> </div> <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 + Stile storage)

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; } /* Storage Section */ #storage-section { width: 90vw; max-width: 800px; margin: 20px auto; background: #1f1f1f; padding: 15px; border-radius: 8px; color: #ccc; text-align: left; } #storage-section div { padding: 5px 0; font-size: 0.9em; } /* Barra di utilizzo disco */ .disk-bar-container { width: 100%; background-color: #333; border-radius: 6px; overflow: hidden; height: 14px; margin-top: 3px; } .disk-bar-fill { height: 14px; background: linear-gradient(90deg, #ffce56, #ffc107); width: 0%; transition: width 0.5s ease; border-radius: 6px 0 0 6px; box-shadow: 0 0 4px #ffc107; font-size: 0.8em; color: #222; text-align: center; line-height: 14px; user-select: none; }

📈 public/script.js (client Socket.io + grafici + gestione storage con barre)

js
document.addEventListener('DOMContentLoaded', () => { const socket = io(); const maxDataPoints = 30; // Funzione per creare un grafico lineare con Chart.js 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 }, } } }); } // Crea tutti i grafici 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', '%'), }; // Aggiorna i dati di un grafico (scorrimento dati) function updateChart(chart, value)