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

Guida aggiornata: Dashboard di monitoraggio server Ubuntu con temperatura CPU da sensors


โœ… Obiettivo

Creare una dashboard web browser-based, tema scuro, responsive e installabile come PWA, per monitorare un server Ubuntu via SSH, con dati in tempo reale via Socket.io.

In questa versione, la temperatura della CPU viene letta con il comando Linux sensors invece che da systeminformation per avere valori piรน affidabili.


๐Ÿงฑ 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)

  • Comando shell sensors (per la temperatura CPU)


๐Ÿ“ 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 + sensors per temperatura CPU)

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 { exec } = require('child_process'); const app = express(); const server = http.createServer(app); const io = socketIo(server); app.use(express.static(path.join(__dirname, 'public'))); // Funzione per leggere temperatura CPU da `sensors` function getCpuTempFromSensors() { return new Promise((resolve, reject) => { exec('sensors -u', (err, stdout, stderr) => { if (err) { return reject(err); } // Estrae la temperatura temp1_input const regex = /temp1_input:\s*([\d.]+)/; const match = stdout.match(regex); if (match) { resolve(parseFloat(match[1])); } else { resolve(0); } }); }); } io.on('connection', (socket) => { console.log('Client connesso'); // Invio dati ogni 2 secondi const interval = setInterval(async () => { try { // Ottieni dati di sistema, esclusa la temperatura CPU const [cpu, mem, disks, net, gpuRaw] = await Promise.all([ si.currentLoad(), si.mem(), si.fsSize(), si.networkStats(), si.graphics() ]); const gpu = gpuRaw.controllers[0] || {}; // Temperatura CPU da sensors const tempCpu = await getCpuTempFromSensors(); // Invia dati via socket 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: disks[0]?.used || 0, diskTotal: disks[0]?.size || 1, temp: tempCpu || 0, netRx: (net[0]?.rx_sec || 0) / 1024, netTx: (net[0]?.tx_sec || 0) / 1024, gpuLoad: gpu.utilizationGpu || 0, gpuTemp: gpu.temperatureGpu || 0 }); // Statistiche disco singolo 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 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}`); });

Gli altri file (public/index.html, public/style.css, public/script.js, manifest.json, service-worker.js) restano invariati rispetto alla versione precedente.


๐Ÿงฉ Nota importante

  • Assicurati di avere installato il pacchetto lm-sensors e che il comando sensors funzioni correttamente nel tuo sistema (sudo apt install lm-sensors + sudo sensors-detect).

  • Il backend esegue il comando sensors ogni 2 secondi per ottenere la temperatura CPU in modo piรน affidabile rispetto a systeminformation.