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)