ff6cf1cd5e
Serveur:
- Modèles Go: NetworkInterface, HardwareInfo dans Agent + AgentMetrics
- DB: migrations network_info_json + hardware_info_json dans agents
- UpsertAgent: stocke les données lentes si présentes dans le payload
- GetAgents: désérialise network_info_json + hardware_info_json
- GET /api/agents/{id}: endpoint single agent
- docker-compose: service iperf3 (port 5201)
Dashboard:
- Popup détail: section RÉSEAU (tableau interfaces: type, vitesse, MAC, WoL, iperf3)
- Popup détail: section HARDWARE (carte mère, CPU, RAM slots/type/vitesse)
- CSS: .net-table/.net-row pour le tableau réseau
- Font-size global appliqué sur html root (au lieu de body)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
139 lines
5.0 KiB
JavaScript
139 lines
5.0 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');
|
|
const verEl = document.getElementById('srv-ver');
|
|
if (verEl && stats.version) verEl.textContent = 'v' + stats.version;
|
|
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.documentElement.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; },
|
|
};
|
|
})();
|