Files
nano_metrics/dashboard/js/app.js
T
Gilles Soulier 22b429f247 feat: dashboard dynamique + RAM min/max dans popup
- Grid: nouvel agent ajouté en temps réel dès le 1er paquet WebSocket (plus besoin d'actualiser la page)
- Grid: ip/status mis à jour depuis chaque metrics_update (adresse DHCP fraîche)
- WS: diffuse agent_removed lors de la suppression d'un agent (sync multi-onglets)
- Popup détail: min/max RAM sur la période affichée (calculé depuis l'historique déjà chargé)
- CSS: classe .chart-minmax pour l'affichage min/max sous le graphe

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 22:53:05 +02:00

137 lines
4.8 KiB
JavaScript

const App = (() => {
let _ws = null;
let _reconnectDelay = 1000;
let _reconnectTimer = null;
let _serverConfig = null;
// Tooltip global position:fixed
const tip = document.getElementById('tooltip');
let _tt;
document.addEventListener('mouseover', e => {
const el = e.target.closest('[data-tip]');
if (!el) return;
clearTimeout(_tt);
_tt = setTimeout(() => {
tip.textContent = el.dataset.tip;
tip.classList.add('show');
}, 120);
});
document.addEventListener('mousemove', e => {
if (!tip.classList.contains('show')) return;
const w = tip.offsetWidth, h = tip.offsetHeight;
let x = e.clientX - w / 2, y = e.clientY - h - 10;
x = Math.max(6, Math.min(x, window.innerWidth - w - 6));
if (y < 6) y = e.clientY + 18;
tip.style.left = x + 'px';
tip.style.top = y + 'px';
});
document.addEventListener('mouseout', e => {
if (!e.target.closest('[data-tip]')) return;
clearTimeout(_tt);
tip.classList.remove('show');
});
function toggleTheme() {
const h = document.documentElement;
h.dataset.theme = h.dataset.theme === 'dark' ? 'light' : 'dark';
document.getElementById('theme-icon').className =
h.dataset.theme === 'dark' ? 'fa-solid fa-moon' : 'fa-solid fa-sun';
}
function updateClock() {
document.getElementById('f-time').textContent =
new Date().toLocaleTimeString('fr-FR');
}
function updateServerStats(stats) {
const cpu = stats.cpu_percent ?? 0;
const memPct = stats.mem_total > 0 ? (stats.mem_used / stats.mem_total * 100) : 0;
const cpuEl = document.getElementById('srv-cpu');
const memEl = document.getElementById('srv-mem');
const cpuBar = document.getElementById('srv-cpu-bar');
const memBar = document.getElementById('srv-mem-bar');
if (cpuEl) {
cpuEl.textContent = cpu.toFixed(0) + '%';
cpuEl.className = 'f-val' + (cpu >= 70 ? ' w' : '');
}
if (cpuBar) {
cpuBar.style.width = cpu.toFixed(0) + '%';
cpuBar.className = 'f-minifill' + (cpu >= 70 ? ' w' : '');
}
if (memEl) {
memEl.textContent = memPct.toFixed(0) + '%';
memEl.className = 'f-val' + (memPct >= 70 ? ' w' : '');
}
if (memBar) {
memBar.style.width = memPct.toFixed(0) + '%';
memBar.className = 'f-minifill' + (memPct >= 70 ? ' w' : '');
}
}
function connectWS() {
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
_ws = new WebSocket(`${proto}://${location.host}/ws`);
_ws.onopen = () => {
_reconnectDelay = 1000;
document.querySelector('.logo-led').style.animation = 'blink 2s infinite';
};
_ws.onmessage = (event) => {
try {
const msg = JSON.parse(event.data);
if (msg.type === 'metrics_update') {
Grid.update(msg.agent_id, msg.data);
updateClock();
} else if (msg.type === 'server_stats') {
updateServerStats(msg.data);
} else if (msg.type === 'status_update') {
Grid.updateStatus(msg.agent_id, msg.data.status);
} else if (msg.type === 'agent_removed') {
Grid.removeAgent(msg.agent_id);
}
} catch {}
};
_ws.onclose = () => {
clearTimeout(_reconnectTimer);
_reconnectTimer = setTimeout(connectWS, _reconnectDelay);
_reconnectDelay = Math.min(_reconnectDelay * 2, 30000);
};
}
async function init() {
try {
_serverConfig = await API.getServerConfig();
if (_serverConfig.tile_min_width) {
document.documentElement.style.setProperty('--tile-min', _serverConfig.tile_min_width + 'px');
}
if (_serverConfig.font_size) {
document.body.style.fontSize = _serverConfig.font_size + 'px';
}
if (_serverConfig.popup_detail_w && _serverConfig.popup_detail_h) {
const pd = document.getElementById('popup-detail');
pd.style.width = _serverConfig.popup_detail_w + 'px';
pd.style.height = _serverConfig.popup_detail_h + 'px';
}
} catch {}
try {
const agents = await API.getAgents();
Grid.refresh(agents);
} catch {}
connectWS();
updateClock();
setInterval(updateClock, 1000);
}
document.addEventListener('DOMContentLoaded', init);
return {
toggleTheme,
get serverConfig() { return _serverConfig; },
set serverConfig(v) { _serverConfig = v; },
};
})();