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; }, }; })();