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
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)
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)
<!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)
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)
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
{
"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
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.
// 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:
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:
<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:
#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;
}
📌 4. Riepilogo
-
si.fsSize()restituisce tutti i filesystem montati npmjs.com. -
Nel backend invii due eventi via Socket:
-
diskSingle→ ogni singolo disco/partizione. -
diskPool→ solo/mnt/storagepoolper riassunto pool.
-
-
Nel frontend, aggiungi un’area dedicata in cui inserire queste info in tempo reale.
-
Nessun plugin aggiuntivo; tutto gestito con
systeminformatione 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
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)
// 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)
<!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)
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)
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)