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

 

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/
│       ├── icon-96.png
│       ├── icon-144.png
│       ├── icon-192.png
│       ├── icon-512.png
│       └── maskable-icon.png
├── 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 strumenti di monitoraggio:

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

    node server.js
  5. Apri nel browser:

    https://<tuo-dominio> o 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 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


🌐 PWA: Progressive Web App

manifest.json

{
  "name": "LoadingCT Dashboard",
  "short_name": "LoadingCT",
  "description": "Monitoraggio in tempo reale del server Linux con CPU, GPU, RAM, disco, rete e temperatura.",
  "start_url": "/",
  "display": "standalone",
  "orientation": "any",
  "background_color": "#121212",
  "theme_color": "#ff6384",
  "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"},
    {"src": "icons/maskable-icon.png","sizes": "192x192","type": "image/png","purpose": "maskable"}
  ]
}
  • HTTPS necessario per installazione su Android (localhost escluso).

service-worker.js

const CACHE_NAME = 'dashboard-cache-v1';
const STATIC_ASSETS = [
  '/', '/index.html', '/style.css', '/script.js', '/manifest.json',
  '/icons/icon-96.png', '/icons/icon-144.png', '/icons/icon-192.png',
  '/icons/icon-512.png', '/icons/maskable-icon.png',
  'https://cdn.jsdelivr.net/npm/chart.js'
];
self.addEventListener('install', e => e.waitUntil(
  caches.open(CACHE_NAME).then(c => c.addAll(STATIC_ASSETS))
));
self.addEventListener('activate', e => e.waitUntil(
  caches.keys().then(keys => Promise.all(
    keys.map(k => k !== CACHE_NAME && caches.delete(k))
  ))
));
self.addEventListener('fetch', e => {
  if (e.request.method !== 'GET') return;
  e.respondWith(
    fetch(e.request).then(r => {
      let rr = r.clone();
      caches.open(CACHE_NAME).then(c => c.put(e.request, rr));
      return r;
    }).catch(() => caches.match(e.request))
  );
});

📱 Responsive e rotazione

Aggiungi al style.css:

/* Smartphone e rotazione */
@media (max-width: 768px) {
  table { display: block; }
  table tr { display: flex; flex-wrap: wrap; justify-content: center; }
  table td { width: 90%; margin-bottom: 20px; height: auto; }
  canvas { height: 140px !important; }
  #bottom-section { flex-direction: column; gap: 20px; padding: 0 10px; }
  #gpu-section, #storage-section { width: 100%; }
}
  • orientation: "any" nel manifest abilita la rotazione del display.


🎉 Conclusione

  • Ora la tua dashboard è PWA installabile e responsive.

  • Servila via HTTPS per vedere il prompt "Aggiungi a schermata Home" su Android.

A cura di Arya (INFN Catania) – Progetto dashboard in real-time