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 viacon SSHtemperatura CPU da sensors


โœ… Obiettivo

Creare una dashboard web browser-based conbased, tema scuro, responsive e visualizzabileinstallabile anchecome su smartphone,PWA, per monitorare un server Ubuntu via SSH.
รˆ una Progressive Web App (PWA) installabile su Android/iOS,SSH, con grafici live e aggiornamentidati in tempo reale via Socket.io.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 serverserver)

  • Socket.io: (comunicazione real-time tra server e clienttime)

  • Chart.js: (grafici dinamici e animatidinamici)

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

  • PWA: (manifest.json, service workerworker, eicone)

    icone
  • Comando shell sensors (per installabilitร la temperatura CPU)


๐Ÿ“ Struttura del progetto

plaintextpgsql
server-server-dashboard/ โ”œโ”€โ”€ public/public/ โ”‚ โ”œโ”€โ”€ index.index.html โ”‚ โ”œโ”€โ”€ style.css โ”‚ โ”œโ”€โ”€ script.js โ”‚ โ”œโ”€โ”€ manifest.json โ”‚ โ”œโ”€โ”€ service-worker.js โ”‚ โ””โ”€โ”€ icons/ โ”‚ โ”œโ”€โ”€ icon-icon-96.png โ”‚ โ”œโ”€โ”€ icon-icon-144.png โ”‚ โ”œโ”€โ”€ icon-icon-192.png โ”‚ โ””โ”€โ”€ icon-icon-512.png โ”œโ”€โ”€ server.server.js โ”œโ”€โ”€ package.json โ””โ”€โ”€ pm2-config.json (opzionale)

โš™๏ธ Codice


๐Ÿ“„ server.js (backend con Socket.io + Express + systeminformation)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 statistichedati di sistemasistema, inesclusa parallelola temperatura CPU 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] || {}; // Temperatura CPU da sensors const tempCpu = await getCpuTempFromSensors(); // Invia dati divia base per graficisocket 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.maintempCpu || 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 montatosingolo 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}`); });

๐Ÿ“„Gli altri file (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, 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 (clientsudo Socket.ioapt install lm-sensors + graficisudo +sensors-detect).

    gestione
  • storage
  • con

    Il barre)backend

    esegue
    js
    il
    comando
    sensors
    ogni
    2
    document.addEventListener('DOMContentLoaded', () => { const socket = io(); const maxDataPoints = 30; // Funzionesecondi per creareottenere unla graficotemperatura 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 %',in '#ff6384',modo '%'),piรน ram:affidabile createChart(documentrispetto a systeminformation.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)