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.

ย 

UPDATE FINAL (si spera)

Guida alla Dashboard Server Web Responsive con Monitoraggio (Node.js + Chart.js)

๐Ÿง  Introduzione

Questa guida descrive come creare una dashboard web responsive, dark theme, per il monitoraggio in tempo reale di un server Ubuntu via SSH. Utilizza Node.js, Express, Socket.io e Chart.js per mostrare dati su CPU, RAM, dischi, rete, temperatura CPU/GPU e carico GPU AMD. Include anche la configurazione come PWA.


๐Ÿ“ Struttura del progetto

server-dashboard/
โ”œโ”€โ”€ public/
โ”‚   โ”œโ”€โ”€ index.html
โ”‚   โ”œโ”€โ”€ script.js
โ”‚   โ”œโ”€โ”€ style.css
โ”‚   โ”œโ”€โ”€ manifest.json
โ”‚   โ”œโ”€โ”€ service-worker.js
โ”‚   โ””โ”€โ”€ icons/
โ”œโ”€โ”€ server.js
โ”œโ”€โ”€ package.json
โ””โ”€โ”€ package-lock.json

๐Ÿš€ Avvio rapido

  1. Installa Node.js:

    sudo apt install nodejs npm
  2. Installa dipendenze:

    npm install express socket.io systeminformation
  3. Installa sensors e radeontop:

    sudo apt install lm-sensors radeontop
    sudo sensors-detect
  4. Avvia la dashboard:

    node server.js
  5. Apri nel browser:

    http://localhost:3000

๐Ÿ”ง Componenti principali

1. server.js โ€“ Backend

  • Recupera dati da systeminformation, sensors, radeontop

  • Emette dati via WebSocket (socket.io)

  • Raccoglie:

    • Carico CPU

    • RAM totale e usata

    • Utilizzo disco (primo disco + storage pool)

    • Rete (rx/tx)

    • Temperatura CPU e GPU (da sensors)

    • Dati dettagliati GPU AMD (da radeontop)

2. index.html โ€“ Frontend

  • Layout a tabella 1x6 per i grafici principali

  • Sezione inferiore con 2 colonne: GPU dettagliata e Storage

  • Responsive, dark theme, icone emoji

  • Include service worker per funzionare come PWA

3. script.js โ€“ Frontend JS

  • Inizializza grafici con Chart.js

  • Riceve dati dal server via WebSocket

  • Aggiorna grafici e visualizzazioni (GPU bar, RAM, discoโ€ฆ)

4. style.css โ€“ Tema dark responsive

  • Layout a tabella per i 6 grafici principali

  • Due colonne responsive in basso (GPU e Storage)

  • Barre GPU dinamiche con tooltip

  • Nessuna scrollbar: sezioni visibili in verticale


๐ŸŒ PWA: Progressive Web App

manifest.json

Configura nome, icona, colore tema, start_url.

service-worker.js

Caching base per offline e installabilitร  su smartphone.


๐Ÿ“ˆ Grafici

Tipo ID HTML Dati
CPU chartCpu % uso CPU
RAM chartRam GB usati
Disco chartDisk GB usati sul primo disco
Temperature chartTemp ยฐC CPU + GPU (grafico doppio)
Rete chartNet KB/s (somma Rx+Tx)

๐Ÿ“Š GPU AMD (radeontop)

  • I dati sono estratti in tempo reale da radeontop -d - -l 1

  • Parametri visualizzati con barre (tooltip descrittivo)

    • gpuLoad, ee, vgt, ta, sx, sh, spi, sc, pa, db, cb, vram, gtt, mclk, sclk


๐Ÿ’พ Storage

  • Ogni disco montato ha barra con %

  • Se presente /mnt/storagepool, viene mostrato separatamente con info GB liberi/occupati


๐Ÿงผ Personalizzazioni finali

  • โœ… Layout 1x6 con table, centrato e responsive

  • โœ… Colonne inferiori piรน alte per mostrare tutta la GPU

  • โœ… Tema dark pulito

  • โœ… Compatibilitร  mobile

  • โœ… Dati testuali sempre visibili sopra ogni grafico


โœ… TODO futuri (opzionale)

  • ย 


A cura di Arya (INFN Catania) โ€“ Progetto dashboard real-time server

ย