1002a6be68
- jetbrains-mono.woff2 et share-tech-mono.woff2 étaient des fichiers HTML (pages 404 téléchargées par erreur) → remplacés par les vrais binaires wOF2 - JetBrains Mono : fichiers séparés regular/bold (400 et 700) - ResizeObserver popup détail : debounce 600ms pour éviter 50+ PUT /api/config lors d'un resize Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
592 lines
35 KiB
JavaScript
592 lines
35 KiB
JavaScript
const Popups = (() => {
|
||
let _currentAgentId = null;
|
||
let _agentCfgData = null;
|
||
let _resizeObs = null;
|
||
let _resizeTimer = null;
|
||
|
||
// ══ POPUP DÉTAIL ══
|
||
async function showDetail(agentId) {
|
||
_currentAgentId = agentId;
|
||
const entry = Grid.getAgent(agentId);
|
||
if (!entry) return;
|
||
const { agent, metrics } = entry;
|
||
|
||
document.getElementById('pop-host').textContent = agent.hostname;
|
||
document.getElementById('pop-ip').textContent = agent.ip || '—';
|
||
const led = document.getElementById('pop-led');
|
||
led.className = 'pop-led';
|
||
led.style.background = agent.status === 'online' ? 'var(--ok)' : 'var(--err)';
|
||
led.style.boxShadow = `0 0 8px ${agent.status === 'online' ? 'var(--ok)' : 'var(--err)'}`;
|
||
|
||
// Icône
|
||
const img = document.getElementById('pop-icon-img');
|
||
const fa = document.getElementById('pop-icon-fa');
|
||
img.src = API.iconUrl(agentId) + '?t=' + Date.now();
|
||
img.style.display = 'block';
|
||
img.onerror = () => { img.style.display = 'none'; fa.style.display = 'flex'; };
|
||
fa.style.display = 'none';
|
||
|
||
// Upload icône
|
||
document.getElementById('pop-icon-wrap').onclick = () => document.getElementById('icon-upload').click();
|
||
document.getElementById('icon-upload').onchange = async (e) => {
|
||
const file = e.target.files[0];
|
||
if (!file) return;
|
||
const hint = document.getElementById('icon-hint');
|
||
try {
|
||
await API.uploadIcon(agentId, file);
|
||
const ts = '?t=' + Date.now();
|
||
img.src = API.iconUrl(agentId) + ts;
|
||
img.style.display = 'block';
|
||
document.getElementById('pop-icon-fa').style.display = 'none';
|
||
const tileImg = document.querySelector(`#tile-${CSS.escape(agentId)} .t-icon img`);
|
||
if (tileImg) tileImg.src = API.iconUrl(agentId) + ts;
|
||
} catch (err) {
|
||
if (hint) {
|
||
hint.style.color = 'var(--err)';
|
||
hint.textContent = 'Erreur : ' + (err.message || 'téléversement échoué');
|
||
setTimeout(() => {
|
||
hint.style.color = '';
|
||
hint.textContent = 'Cliquer sur l\'icône pour personnaliser · JPG PNG WEBP · max 128×128 px';
|
||
}, 4000);
|
||
}
|
||
} finally {
|
||
e.target.value = '';
|
||
}
|
||
};
|
||
|
||
// Uptime
|
||
const up = metrics?.uptime;
|
||
if (up) {
|
||
const d = Math.floor(up / 86400), h = Math.floor((up % 86400) / 3600);
|
||
document.getElementById('pop-uptime').innerHTML =
|
||
`<i class="fa-solid fa-clock" style="margin-right:4px"></i>En ligne depuis ${d}j ${h}h`;
|
||
}
|
||
|
||
// Corps du popup
|
||
const now = Math.floor(Date.now() / 1000);
|
||
let history = [];
|
||
try { history = await API.getAgentHistory(agentId, now - 1800, now); } catch {}
|
||
|
||
const cpuPts = Charts.historyToCpuPts(history);
|
||
const memPts = Charts.historyToMemPts(history);
|
||
|
||
let ramMin = null, ramMax = null;
|
||
for (const h of history) {
|
||
if (h.memory_used != null) {
|
||
if (ramMin === null || h.memory_used < ramMin) ramMin = h.memory_used;
|
||
if (ramMax === null || h.memory_used > ramMax) ramMax = h.memory_used;
|
||
}
|
||
}
|
||
const ramMinMax = ramMin !== null
|
||
? `<div class="chart-minmax"><span>min ${Grid.fmt(ramMin)}</span><span>max ${Grid.fmt(ramMax)}</span></div>`
|
||
: '';
|
||
|
||
const smartBadges = metrics?.smart?.length > 0
|
||
? '<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:6px">' +
|
||
metrics.smart.map((s, i) => {
|
||
const st = Grid.smartState(s);
|
||
const lbl = { ok: 'OK', old: 'USAGÉ', prefail: 'PREFAIL', hs: 'HS' }[st];
|
||
return `<span class="smart-pill ${st}"
|
||
onclick="Popups.showSmart('${esc(agentId)}',${i})"
|
||
data-tip="Santé SMART de ${esc(s.device)}">
|
||
<i class="fa-solid fa-hard-drive" style="font-size:8px"></i>
|
||
${esc(s.device)} · ${lbl}
|
||
</span>`;
|
||
}).join('') + '</div>'
|
||
: '';
|
||
|
||
const protos = [
|
||
metrics?.cpu_percent != null ? `<span class="proto-badge udp"><i class="fa-solid fa-arrow-up"></i>UDP</span>` : '',
|
||
].filter(Boolean).join('');
|
||
|
||
document.getElementById('pop-body').innerHTML = `
|
||
<div>
|
||
<div class="sec-title">MÉTRIQUES ACTUELLES</div>
|
||
<div class="kpi-grid">
|
||
<div class="kpi"><div class="kpi-lbl">CPU</div>
|
||
<div class="kpi-val c-ok">${(metrics?.cpu_percent ?? 0).toFixed(0)}<span class="u">%</span></div></div>
|
||
<div class="kpi"><div class="kpi-lbl">MÉMOIRE</div>
|
||
<div class="kpi-val">${Grid.fmt(metrics?.memory_used)}</div>
|
||
<div class="kpi-sub">/ ${Grid.fmt(metrics?.memory_total)}</div></div>
|
||
<div class="kpi"><div class="kpi-lbl">DISQUE</div>
|
||
<div class="kpi-val">${Grid.fmt(metrics?.hdd_used)}</div>
|
||
<div class="kpi-sub">/ ${Grid.fmt(metrics?.hdd_total)}</div></div>
|
||
<div class="kpi"><div class="kpi-lbl">UPTIME</div>
|
||
<div class="kpi-val" style="font-size:15px">${document.getElementById('pop-uptime').textContent.replace(/.*depuis /,'')}</div></div>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div class="sec-title">HISTORIQUE — 30 MIN</div>
|
||
<div class="charts-grid">
|
||
<div class="chart-card">
|
||
<div class="chart-header">
|
||
<div class="chart-label" style="color:var(--accent)"><i class="fa-solid fa-microchip"></i>CPU</div>
|
||
<span class="chart-cur c-ok">${(metrics?.cpu_percent ?? 0).toFixed(0)}%</span>
|
||
</div>
|
||
<svg class="chart-svg" viewBox="0 0 200 52" preserveAspectRatio="none" id="det-cpu-chart"></svg>
|
||
<div class="chart-axis"><span>−30min</span><span>−15min</span><span>now</span></div>
|
||
</div>
|
||
<div class="chart-card">
|
||
<div class="chart-header">
|
||
<div class="chart-label" style="color:var(--blue)"><i class="fa-solid fa-memory"></i>RAM</div>
|
||
<div style="display:flex;align-items:baseline;gap:5px">
|
||
<span class="chart-cur" style="color:var(--blue)">${Grid.fmtPct(metrics?.memory_used && metrics?.memory_total ? metrics.memory_used / metrics.memory_total * 100 : null)}</span>
|
||
<span style="font-family:var(--font-mono);font-size:10px;color:var(--ink-3)">${Grid.fmt(metrics?.memory_used)}</span>
|
||
</div>
|
||
</div>
|
||
<svg class="chart-svg" viewBox="0 0 200 52" preserveAspectRatio="none" id="det-mem-chart"></svg>
|
||
<div class="chart-axis"><span>−30min</span><span>−15min</span><span>now</span></div>
|
||
${ramMinMax}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div class="sec-title">STOCKAGE</div>
|
||
<div style="display:flex;flex-direction:column;gap:8px">
|
||
<div style="display:flex;align-items:center;gap:10px">
|
||
<div style="width:22px;text-align:center;font-size:13px;cursor:help" data-tip="Utilisé"><i class="fa-solid fa-hard-drive"></i></div>
|
||
<div style="flex:1;height:7px;border-radius:4px;background:var(--bg-1);overflow:hidden">
|
||
<div style="height:100%;border-radius:4px;background:var(--ok);width:${metrics?.hdd_total ? (metrics.hdd_used/metrics.hdd_total*100).toFixed(0) : 0}%"></div></div>
|
||
<span style="font-family:var(--font-mono);font-size:12px;color:var(--ink-2);width:90px;text-align:right">${Grid.fmt(metrics?.hdd_used)} / ${Grid.fmt(metrics?.hdd_total)}</span>
|
||
</div>
|
||
${smartBadges}
|
||
</div>
|
||
</div>
|
||
${(() => {
|
||
const ni = entry?.agent?.network_info;
|
||
if (!ni?.length) return '';
|
||
const wol = v => v == null ? '—' : v ? '<span style="color:var(--ok)">Oui</span>' : '<span style="color:var(--ink-4)">Non</span>';
|
||
const spd = v => v == null ? '—' : v >= 1000 ? '1 Gb' : v + ' Mb';
|
||
const rows = ni.map(iface => `
|
||
<div class="net-row">
|
||
<span style="color:var(--ink-3);font-size:12px"><i class="fa-solid fa-${iface.if_type === 'wifi' ? 'wifi' : 'ethernet'}"></i></span>
|
||
<span style="color:var(--ink-1);font-weight:600">${esc(iface.name)}</span>
|
||
<span style="color:var(--ink-3)">${spd(iface.speed_mbps)}</span>
|
||
<span style="color:var(--ink-4);font-size:9px;letter-spacing:.04em">${esc(iface.mac)}</span>
|
||
<span>WoL : ${wol(iface.wol)}</span>
|
||
<span style="color:var(--blue)">${iface.iperf_mbps != null ? iface.iperf_mbps.toFixed(1) + ' Mb/s' : '—'}</span>
|
||
</div>`).join('');
|
||
return `<div>
|
||
<div class="sec-title">RÉSEAU</div>
|
||
<div class="net-table">
|
||
<div class="net-row" style="background:var(--bg-4);font-size:9px;color:var(--ink-4);letter-spacing:.06em">
|
||
<span></span><span>INTERFACE</span><span>VITESSE</span><span>MAC</span><span>WAKE ON LAN</span><span>IPERF3</span>
|
||
</div>
|
||
${rows}
|
||
</div>
|
||
</div>`;
|
||
})()}
|
||
${(() => {
|
||
const hw = entry?.agent?.hardware_info;
|
||
if (!hw) return '';
|
||
const row = (lbl, val) => val ? `<div class="meta"><div class="meta-lbl">${lbl}</div><div class="meta-val">${esc(String(val))}</div></div>` : '';
|
||
const ramSlots = hw.ram_slots_used != null && hw.ram_slots_total != null
|
||
? `${hw.ram_slots_used}/${hw.ram_slots_total} slots` : null;
|
||
const ramInfo = [hw.ram_type, hw.ram_speed_mhz ? hw.ram_speed_mhz + ' MHz' : null, ramSlots]
|
||
.filter(Boolean).join(' · ') || null;
|
||
return `<div>
|
||
<div class="sec-title">HARDWARE</div>
|
||
<div class="meta-grid">
|
||
${row('CARTE MÈRE', hw.motherboard_vendor && hw.motherboard_model ? hw.motherboard_vendor + ' ' + hw.motherboard_model : hw.motherboard_model || hw.motherboard_vendor)}
|
||
${row('PROCESSEUR', hw.cpu_model)}
|
||
${row('MÉMOIRE RAM', ramInfo)}
|
||
</div>
|
||
</div>`;
|
||
})()}
|
||
<div>
|
||
<div class="sec-title">INFORMATIONS</div>
|
||
<div class="meta-grid">
|
||
<div class="meta"><div class="meta-lbl">HOSTNAME</div><div class="meta-val">${esc(agent.hostname)}</div></div>
|
||
<div class="meta"><div class="meta-lbl">ADRESSE IP</div><div class="meta-val">${esc(agent.ip) || '—'}</div></div>
|
||
<div class="meta"><div class="meta-lbl">VERSION AGENT</div><div class="meta-val" style="display:flex;align-items:center;gap:6px">
|
||
${agent.version ? `<span style="font-family:var(--font-mono);background:var(--bg-1);border:1px solid var(--border-2);border-radius:5px;padding:1px 7px;font-size:11px;color:var(--accent)">v${esc(agent.version)}</span>` : '<span style="color:var(--ink-4)">—</span>'}
|
||
</div></div>
|
||
<div class="meta"><div class="meta-lbl">PROTOCOLES ACTIFS</div><div style="display:flex;gap:5px;margin-top:4px">${protos || '—'}</div></div>
|
||
<div class="meta"><div class="meta-lbl">DERNIER CONTACT</div><div class="meta-val">${new Date(agent.last_seen * 1000).toLocaleTimeString('fr-FR')}</div></div>
|
||
</div>
|
||
</div>`;
|
||
|
||
requestAnimationFrame(() => {
|
||
Charts.renderChart(document.getElementById('det-cpu-chart'), cpuPts, '--accent');
|
||
Charts.renderChart(document.getElementById('det-mem-chart'), memPts, '--blue');
|
||
});
|
||
|
||
// Resize → sauvegarder sur serveur
|
||
if (_resizeObs) _resizeObs.disconnect();
|
||
const pd = document.getElementById('popup-detail');
|
||
_resizeObs = new ResizeObserver(() => {
|
||
clearTimeout(_resizeTimer);
|
||
_resizeTimer = setTimeout(() => {
|
||
API.putServerConfig({
|
||
...App.serverConfig,
|
||
popup_detail_w: pd.offsetWidth,
|
||
popup_detail_h: pd.offsetHeight,
|
||
}).catch(() => {});
|
||
}, 600);
|
||
});
|
||
_resizeObs.observe(pd);
|
||
|
||
document.getElementById('overlay-detail').style.display = 'flex';
|
||
}
|
||
|
||
function hideDetail() {
|
||
document.getElementById('overlay-detail').style.display = 'none';
|
||
}
|
||
|
||
// ══ CONFIG AGENT ══
|
||
async function showAgentCfg() {
|
||
if (!_currentAgentId) return;
|
||
let cfg = {};
|
||
try { cfg = await API.getAgentConfig(_currentAgentId); } catch {}
|
||
_agentCfgData = cfg;
|
||
|
||
document.getElementById('agentcfg-sub').textContent =
|
||
`${_currentAgentId} · config récupérée`;
|
||
|
||
const metrics = ['cpu','memory','disk','smart','uptime','network','temperature'];
|
||
const icons = {
|
||
cpu:'fa-microchip',memory:'fa-memory',disk:'fa-hard-drive',
|
||
smart:'fa-shield-heart',uptime:'fa-clock',network:'fa-network-wired',
|
||
temperature:'fa-thermometer-half'
|
||
};
|
||
const mqttCfg = cfg.protocols?.mqtt ?? {};
|
||
|
||
document.getElementById('agentcfg-body').innerHTML = `
|
||
<div style="display:flex;flex-direction:column;gap:8px">
|
||
<div style="font-size:9px;color:var(--ink-4);font-family:var(--font-terminal);letter-spacing:.08em;padding-bottom:6px;border-bottom:1px solid var(--border-1)">MÉTRIQUES PAR PROTOCOLE</div>
|
||
<div class="metrics-table">
|
||
<div class="metrics-header">
|
||
<span class="mh-label">MÉTRIQUE</span>
|
||
<span class="mh-proto udp"><i class="fa-solid fa-arrow-up"></i> UDP</span>
|
||
<span class="mh-proto mqtt"><i class="fa-solid fa-tower-broadcast" style="font-size:8px"></i> MQTT</span>
|
||
</div>
|
||
${metrics.map(m => {
|
||
const udpOn = cfg.metrics?.[m]?.udp ? 'udp-on' : '';
|
||
const mqttOn = cfg.metrics?.[m]?.mqtt ? 'mqtt-on' : '';
|
||
return `<div class="metric-row">
|
||
<div class="metric-cell">
|
||
<div class="metric-ico"><i class="fa-solid ${icons[m]}"></i></div>
|
||
<span class="metric-name">${m}</span>
|
||
</div>
|
||
<div class="metric-chk"><div class="cbox ${udpOn}" id="cbox-${m}-udp" onclick="Popups.toggleCbox(this,'${m}','udp')" data-tip="${m} via UDP"><i class="fa-solid fa-check"></i></div></div>
|
||
<div class="metric-chk"><div class="cbox ${mqttOn}" id="cbox-${m}-mqtt" onclick="Popups.toggleCbox(this,'${m}','mqtt')" data-tip="${m} via MQTT"><i class="fa-solid fa-check"></i></div></div>
|
||
</div>`;
|
||
}).join('')}
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;flex-direction:column;gap:8px">
|
||
<div style="font-size:9px;color:var(--ink-4);font-family:var(--font-terminal);letter-spacing:.08em;padding-bottom:6px;border-bottom:1px solid var(--border-1)">PARAMÈTRES MQTT</div>
|
||
<div style="background:var(--bg-3);border-radius:8px;border:1px solid rgba(200,130,200,.2);padding:12px 14px;display:flex;flex-direction:column;gap:10px">
|
||
<div style="display:flex;align-items:center;gap:10px"><label style="font-size:11px;color:var(--ink-3);font-family:var(--font-terminal);width:90px">Broker</label>
|
||
<input id="mqtt-host" style="flex:1;background:var(--bg-1);border:1px solid var(--border-2);border-radius:6px;color:var(--ink-1);padding:6px 10px;font-size:12px;font-family:var(--font-mono)" value="${mqttCfg.host ?? '10.0.0.3'}"></div>
|
||
<div style="display:flex;align-items:center;gap:10px"><label style="font-size:11px;color:var(--ink-3);font-family:var(--font-terminal);width:90px">Port</label>
|
||
<input id="mqtt-port" type="number" style="width:90px;background:var(--bg-1);border:1px solid var(--border-2);border-radius:6px;color:var(--ink-1);padding:6px 10px;font-size:12px;font-family:var(--font-mono)" value="${mqttCfg.port ?? 1883}"></div>
|
||
<div style="display:flex;align-items:center;gap:10px"><label style="font-size:11px;color:var(--ink-3);font-family:var(--font-terminal);width:90px">Topic base</label>
|
||
<input id="mqtt-topic" style="flex:1;background:var(--bg-1);border:1px solid var(--border-2);border-radius:6px;color:var(--ink-1);padding:6px 10px;font-size:12px;font-family:var(--font-mono)" value="${mqttCfg.topic_base ?? 'nanometrics/agents'}"></div>
|
||
<div style="border-top:1px solid var(--border-1);padding-top:8px;display:flex;flex-direction:column;gap:5px">
|
||
${['auto_discovery:Auto-discovery (Home Assistant):fa-satellite-dish',
|
||
'birth_message:Birth message:fa-arrow-right-to-bracket',
|
||
'last_will:Last Will message:fa-skull'].map(s => {
|
||
const [key, label, icon] = s.split(':');
|
||
return `<div style="display:flex;align-items:center;justify-content:space-between;padding:3px 0">
|
||
<label style="font-size:12px;color:var(--ink-2);display:flex;align-items:center;gap:7px;cursor:pointer">
|
||
<i class="fa-solid ${icon}" style="color:var(--purple);font-size:11px"></i>${label}
|
||
</label>
|
||
<label class="toggle">
|
||
<input type="checkbox" id="mqtt-${key}" ${mqttCfg[key] !== false ? 'checked' : ''}>
|
||
<span class="toggle-slider"></span>
|
||
</label>
|
||
</div>`;
|
||
}).join('')}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;flex-direction:column;gap:8px">
|
||
<div style="font-size:9px;color:var(--ink-4);font-family:var(--font-terminal);letter-spacing:.08em;padding-bottom:6px;border-bottom:1px solid var(--border-1)">
|
||
COMMANDES DISTANTES <span style="color:var(--ink-4);font-size:8px;margin-left:6px">— BIENTÔT</span>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:6px">
|
||
${[['fa-rotate-right','reboot'],['fa-power-off','shutdown'],['fa-display','screen off'],
|
||
['fa-arrow-up-from-bracket','update'],['fa-arrow-up-right-dots','upgrade'],['fa-terminal','shell cmd']].map(
|
||
([icon, label]) => `<div style="display:flex;flex-direction:column;align-items:center;gap:4px;padding:10px 8px;border-radius:8px;background:var(--bg-3);border:1px solid var(--border-1);cursor:not-allowed;opacity:.4">
|
||
<i class="fa-solid ${icon}" style="font-size:16px;color:var(--ink-3)"></i>
|
||
<span style="font-size:10px;color:var(--ink-4);font-family:var(--font-terminal)">${label}</span>
|
||
<span style="font-size:8px;color:var(--ink-4);font-family:var(--font-terminal)">bientôt</span>
|
||
</div>`).join('')}
|
||
</div>
|
||
</div>`;
|
||
|
||
document.getElementById('overlay-agentcfg').style.display = 'flex';
|
||
}
|
||
|
||
function toggleCbox(el, metric, proto) {
|
||
const isOn = el.classList.contains(proto + '-on');
|
||
el.classList.toggle(proto + '-on', !isOn);
|
||
el.style.color = isOn ? 'transparent' : '';
|
||
if (!_agentCfgData.metrics) _agentCfgData.metrics = {};
|
||
if (!_agentCfgData.metrics[metric]) _agentCfgData.metrics[metric] = {};
|
||
_agentCfgData.metrics[metric][proto] = !isOn;
|
||
}
|
||
|
||
async function sendAgentConfig() {
|
||
if (!_currentAgentId || !_agentCfgData) return;
|
||
if (!_agentCfgData.protocols) _agentCfgData.protocols = {};
|
||
_agentCfgData.protocols.mqtt = {
|
||
..._agentCfgData.protocols.mqtt,
|
||
host: document.getElementById('mqtt-host')?.value ?? '10.0.0.3',
|
||
port: parseInt(document.getElementById('mqtt-port')?.value ?? '1883'),
|
||
topic_base: document.getElementById('mqtt-topic')?.value ?? 'nanometrics/agents',
|
||
auto_discovery: document.getElementById('mqtt-auto_discovery')?.checked ?? true,
|
||
birth_message: document.getElementById('mqtt-birth_message')?.checked ?? true,
|
||
last_will: document.getElementById('mqtt-last_will')?.checked ?? true,
|
||
};
|
||
try {
|
||
await API.putAgentConfig(_currentAgentId, _agentCfgData);
|
||
document.getElementById('overlay-agentcfg').style.display = 'none';
|
||
} catch (e) {
|
||
alert('Erreur lors de l\'envoi : ' + e.message);
|
||
}
|
||
}
|
||
|
||
// ══ CONFIG SERVEUR ══
|
||
async function showSrvCfg() {
|
||
const cfg = App.serverConfig ?? {};
|
||
document.getElementById('btn-srvcfg').classList.add('active-btn');
|
||
document.getElementById('srvcfg-body').innerHTML = `
|
||
<div style="display:flex;flex-direction:column;gap:8px">
|
||
<div class="scfg-sec-title">AFFICHAGE DES TUILES</div>
|
||
<div class="scfg-row"><label>Largeur min.</label>
|
||
<input type="range" class="scfg-slider" min="160" max="420" value="${cfg.tile_min_width ?? 220}"
|
||
oninput="this.nextElementSibling.textContent=this.value+'px'" id="s-tile-w">
|
||
<span class="scfg-val">${cfg.tile_min_width ?? 220}px</span></div>
|
||
<div class="scfg-row"><label>Taille du texte</label>
|
||
<input type="range" class="scfg-slider" min="10" max="18" value="${cfg.font_size ?? 13}"
|
||
oninput="this.nextElementSibling.textContent=this.value+'px'" id="s-font">
|
||
<span class="scfg-val">${cfg.font_size ?? 13}px</span></div>
|
||
<div class="scfg-row"><label>Type de jauge</label>
|
||
<select class="scfg-select" id="s-gauge-type">
|
||
<option value="compact" ${(cfg.gauge_type ?? 'compact') === 'compact' ? 'selected' : ''}>Compact</option>
|
||
<option value="standard" ${(cfg.gauge_type ?? 'compact') === 'standard' ? 'selected' : ''}>Standard</option>
|
||
</select></div>
|
||
</div>
|
||
<div style="display:flex;flex-direction:column;gap:8px">
|
||
<div class="scfg-sec-title">SEUILS D'ALERTE</div>
|
||
<div class="scfg-row"><label>Warning CPU/RAM</label>
|
||
<input type="range" class="scfg-slider" min="50" max="95" value="${cfg.warn_cpu ?? 70}"
|
||
oninput="this.nextElementSibling.textContent=this.value+'%'" id="s-warn-cpu">
|
||
<span class="scfg-val">${cfg.warn_cpu ?? 70}%</span></div>
|
||
<div class="scfg-row"><label>Erreur CPU/RAM</label>
|
||
<input type="range" class="scfg-slider" min="60" max="100" value="${cfg.err_cpu ?? 85}"
|
||
oninput="this.nextElementSibling.textContent=this.value+'%'" id="s-err-cpu">
|
||
<span class="scfg-val">${cfg.err_cpu ?? 85}%</span></div>
|
||
<div class="scfg-row"><label>Warning Disque</label>
|
||
<input type="range" class="scfg-slider" min="50" max="95" value="${cfg.warn_disk ?? 75}"
|
||
oninput="this.nextElementSibling.textContent=this.value+'%'" id="s-warn-disk">
|
||
<span class="scfg-val">${cfg.warn_disk ?? 75}%</span></div>
|
||
</div>
|
||
<div style="display:flex;flex-direction:column;gap:8px">
|
||
<div class="scfg-sec-title">DONNÉES & RÉTENTION</div>
|
||
<div class="scfg-row"><label>Historique</label>
|
||
<select class="scfg-select" id="s-retention">
|
||
${[7,30,90,365].map(d => `<option value="${d}" ${(cfg.retention_days??30)==d?'selected':''}>${d} jours</option>`).join('')}
|
||
</select></div>
|
||
<div class="scfg-row"><label>Courbes (durée)</label>
|
||
<select class="scfg-select" id="s-chart-dur">
|
||
${[[15,'15 min'],[30,'30 min'],[60,'1 heure'],[360,'6 heures']].map(([v,l]) =>
|
||
`<option value="${v}" ${(cfg.chart_duration_min??30)==v?'selected':''}>${l}</option>`).join('')}
|
||
</select></div>
|
||
</div>
|
||
<div style="display:flex;flex-direction:column;gap:8px">
|
||
<div class="scfg-sec-title">INSTALLATION AGENT</div>
|
||
<div class="scfg-row" style="flex-direction:column;align-items:stretch;gap:6px">
|
||
<div style="display:flex;gap:6px;align-items:center">
|
||
<input type="text" id="s-install-cmd" readonly
|
||
style="flex:1;font-family:var(--font-mono);font-size:0.78em;padding:6px 8px;
|
||
background:var(--bg2);border:1px solid var(--border);border-radius:6px;
|
||
color:var(--fg);cursor:text;min-width:0"
|
||
value="SERVER_IP=${window.location.hostname} curl -fsSL https://git.maison43gil.com/gilles/nano_metrics/raw/branch/main/deploy/install.sh | sudo bash">
|
||
<button class="btn" style="padding:5px 10px;font-size:0.8em;white-space:nowrap"
|
||
onclick="Popups._copyInstallCmd(this)">Copier</button>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
document.getElementById('overlay-srvcfg').style.display = 'flex';
|
||
}
|
||
|
||
function hideSrvCfg() {
|
||
document.getElementById('overlay-srvcfg').style.display = 'none';
|
||
document.getElementById('btn-srvcfg').classList.remove('active-btn');
|
||
}
|
||
|
||
async function saveSrvCfg() {
|
||
const cfg = {
|
||
...App.serverConfig,
|
||
tile_min_width: parseInt(document.getElementById('s-tile-w')?.value ?? 220),
|
||
font_size: parseInt(document.getElementById('s-font')?.value ?? 13),
|
||
warn_cpu: parseInt(document.getElementById('s-warn-cpu')?.value ?? 70),
|
||
err_cpu: parseInt(document.getElementById('s-err-cpu')?.value ?? 85),
|
||
warn_disk: parseInt(document.getElementById('s-warn-disk')?.value ?? 75),
|
||
retention_days: parseInt(document.getElementById('s-retention')?.value ?? 30),
|
||
chart_duration_min: parseInt(document.getElementById('s-chart-dur')?.value ?? 30),
|
||
gauge_type: document.getElementById('s-gauge-type')?.value ?? 'compact',
|
||
};
|
||
const prevGaugeType = App.serverConfig?.gauge_type ?? 'compact';
|
||
await API.putServerConfig(cfg);
|
||
App.serverConfig = cfg;
|
||
document.documentElement.style.setProperty('--tile-min', cfg.tile_min_width + 'px');
|
||
document.body.style.fontSize = cfg.font_size + 'px';
|
||
if (cfg.gauge_type !== prevGaugeType) Grid.rerenderAll();
|
||
hideSrvCfg();
|
||
}
|
||
|
||
// ══ POPUP SMART ══
|
||
function showSmart(agentId, diskIdx = 0) {
|
||
const smartList = Grid.getAgent(agentId)?.metrics?.smart;
|
||
if (!smartList?.length) return;
|
||
const m = smartList[diskIdx] ?? smartList[0];
|
||
const state = Grid.smartState(m);
|
||
|
||
document.getElementById('smart-sub').textContent = m.device ? `${agentId} — ${m.device}` : agentId;
|
||
|
||
const stateInfo = {
|
||
ok: { color:'var(--ok)', bg:'rgba(77,187,38,.1)', border:'rgba(77,187,38,.3)', icon:'fa-circle-check',
|
||
title:'Disque en bonne santé',
|
||
desc:'Aucun problème détecté. Votre disque fonctionne normalement.' },
|
||
old: { color:'var(--warn)', bg:'rgba(250,189,47,.1)', border:'rgba(250,189,47,.3)', icon:'fa-clock-rotate-left',
|
||
title:'Disque ancien ou très utilisé',
|
||
desc:'Votre disque fonctionne encore, mais il a accumulé beaucoup d\'heures. Pensez à prévoir un remplacement.' },
|
||
prefail: { color:'var(--accent)', bg:'var(--accent-tint)', border:'rgba(254,128,25,.3)', icon:'fa-triangle-exclamation',
|
||
title:'Signes de défaillance imminente',
|
||
desc:'Ce disque présente des indicateurs préoccupants. Sauvegardez vos données dès maintenant et envisagez un remplacement rapide.' },
|
||
hs: { color:'var(--err)', bg:'rgba(251,73,52,.1)', border:'rgba(251,73,52,.3)', icon:'fa-circle-xmark',
|
||
title:'Disque défaillant',
|
||
desc:'Ce disque a échoué au test SMART. Il peut tomber en panne à tout moment. Sauvegardez immédiatement et remplacez-le.' },
|
||
};
|
||
const si = stateInfo[state];
|
||
|
||
const tempColor = m.temperature == null ? null
|
||
: m.temperature > 60 ? 'var(--err)' : m.temperature > 50 ? 'var(--warn)' : 'var(--ok)';
|
||
const tempLabel = m.temperature == null ? null
|
||
: m.temperature > 60 ? 'Critique' : m.temperature > 50 ? 'Élevée' : 'Normale';
|
||
const tempBg = tempColor === 'var(--ok)' ? 'rgba(77,187,38,.15)'
|
||
: tempColor === 'var(--warn)' ? 'rgba(250,189,47,.15)' : 'rgba(251,73,52,.15)';
|
||
|
||
const secColor = m.reallocated_sectors == null ? null
|
||
: m.reallocated_sectors === 0 ? 'var(--ok)' : m.reallocated_sectors < 10 ? 'var(--warn)' : 'var(--err)';
|
||
const secDesc = m.reallocated_sectors === 0
|
||
? 'Aucun secteur défectueux — parfait.'
|
||
: m.reallocated_sectors < 10 ? 'Quelques secteurs remplacés. Surveillez l\'évolution.'
|
||
: 'Nombreux secteurs défectueux — risque de panne élevé.';
|
||
|
||
const hoursColor = m.power_on_hours == null ? null
|
||
: m.power_on_hours > 40000 ? 'var(--err)' : m.power_on_hours > 25000 ? 'var(--warn)' : 'var(--ok)';
|
||
|
||
const wearColor = m.wear_level == null ? null
|
||
: m.wear_level < 20 ? 'var(--err)' : m.wear_level < 50 ? 'var(--warn)' : 'var(--ok)';
|
||
const wearDesc = m.wear_level == null ? ''
|
||
: m.wear_level >= 80 ? 'Très bonne durée de vie restante.'
|
||
: m.wear_level >= 50 ? 'Durée de vie acceptable, à surveiller.'
|
||
: m.wear_level >= 20 ? 'Durée de vie réduite — pensez au remplacement.'
|
||
: 'Durée de vie critique — remplacez ce SSD rapidement.';
|
||
|
||
document.getElementById('smart-body').innerHTML = `
|
||
<div class="smart-verdict" style="background:${si.bg};border-color:${si.border}">
|
||
<div style="font-size:28px;color:${si.color}"><i class="fa-solid ${si.icon}"></i></div>
|
||
<div>
|
||
<div style="font-size:16px;font-weight:700;color:${si.color}">${si.title}</div>
|
||
<div style="font-size:12px;color:var(--ink-3);margin-top:3px">${si.desc}</div>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div class="sec-title">POINTS DE CONTRÔLE</div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
|
||
${m.temperature != null ? `<div style="background:var(--bg-3);border-radius:8px;padding:12px 14px;border:1px solid var(--border-1)">
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
|
||
<span style="color:${tempColor};font-size:14px;width:22px;text-align:center"><i class="fa-solid fa-temperature-half"></i></span>
|
||
<span style="font-weight:600;font-size:12px;flex:1">Température</span>
|
||
<span style="font-size:10px;font-family:var(--font-terminal);font-weight:700;padding:1px 7px;border-radius:999px;background:${tempBg};color:${tempColor}">${tempLabel}</span>
|
||
</div>
|
||
<div class="si-val">${m.temperature}<span class="u">°C</span></div>
|
||
<div class="si-desc">Normale entre 20–50°C. Au-delà de 60°C le disque risque de s'abîmer.</div>
|
||
</div>` : ''}
|
||
${m.reallocated_sectors != null ? `<div style="background:var(--bg-3);border-radius:8px;padding:12px 14px;border:1px solid var(--border-1)">
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
|
||
<span style="color:${secColor};font-size:14px;width:22px;text-align:center"><i class="fa-solid ${m.reallocated_sectors === 0 ? 'fa-circle-check' : 'fa-circle-exclamation'}"></i></span>
|
||
<span style="font-weight:600;font-size:12px;flex:1">Secteurs défectueux</span>
|
||
</div>
|
||
<div class="si-val">${m.reallocated_sectors}<span class="u"> sect.</span></div>
|
||
<div class="si-desc">${secDesc}</div>
|
||
</div>` : ''}
|
||
${m.power_on_hours != null ? `<div style="background:var(--bg-3);border-radius:8px;padding:12px 14px;border:1px solid var(--border-1)">
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
|
||
<span style="color:${hoursColor};font-size:14px;width:22px;text-align:center"><i class="fa-solid fa-clock-rotate-left"></i></span>
|
||
<span style="font-weight:600;font-size:12px;flex:1">Durée de fonctionnement</span>
|
||
</div>
|
||
<div class="si-val">${m.power_on_hours.toLocaleString('fr-FR')}<span class="u"> h</span></div>
|
||
<div class="si-desc">≈${Math.floor(m.power_on_hours / 24)} jours d'utilisation. Un disque dur dure en moyenne 3 à 5 ans (25 000–40 000 h).</div>
|
||
</div>` : ''}
|
||
${m.wear_level != null ? `<div style="background:var(--bg-3);border-radius:8px;padding:12px 14px;border:1px solid var(--border-1)">
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
|
||
<span style="color:${wearColor};font-size:14px;width:22px;text-align:center"><i class="fa-solid fa-battery-full"></i></span>
|
||
<span style="font-weight:600;font-size:12px;flex:1">Durée de vie SSD restante</span>
|
||
</div>
|
||
<div class="si-val">${m.wear_level}<span class="u">%</span></div>
|
||
<div class="si-desc">${wearDesc}</div>
|
||
</div>` : ''}
|
||
</div>
|
||
</div>`;
|
||
|
||
document.getElementById('overlay-smart').style.display = 'flex';
|
||
}
|
||
|
||
// ══ SUPPRESSION AGENT ══
|
||
let _delAgentId = null;
|
||
|
||
function confirmDeleteAgent(id, hostname) {
|
||
_delAgentId = id;
|
||
document.getElementById('del-agent-name').textContent = hostname || id;
|
||
document.getElementById('overlay-del').style.display = 'flex';
|
||
}
|
||
|
||
async function doDeleteAgent() {
|
||
if (!_delAgentId) return;
|
||
const id = _delAgentId;
|
||
document.getElementById('overlay-del').style.display = 'none';
|
||
_delAgentId = null;
|
||
try {
|
||
await API.deleteAgent(id);
|
||
Grid.removeAgent(id);
|
||
} catch (e) {
|
||
alert('Erreur lors de la suppression : ' + e.message);
|
||
}
|
||
}
|
||
|
||
function _copyInstallCmd(btn) {
|
||
const text = document.getElementById('s-install-cmd').value;
|
||
const done = () => { btn.textContent = '✓ Copié'; setTimeout(() => btn.textContent = 'Copier', 1500); };
|
||
if (window.isSecureContext && navigator.clipboard?.writeText) {
|
||
navigator.clipboard.writeText(text).then(done).catch(() => _copyFallback(text, done));
|
||
} else {
|
||
_copyFallback(text, done);
|
||
}
|
||
}
|
||
|
||
function _copyFallback(text, cb) {
|
||
const ta = document.createElement('textarea');
|
||
ta.value = text;
|
||
ta.style.cssText = 'position:fixed;opacity:0;top:0;left:0';
|
||
document.body.appendChild(ta);
|
||
ta.focus();
|
||
ta.select();
|
||
try { document.execCommand('copy'); cb(); } catch (_) {}
|
||
document.body.removeChild(ta);
|
||
}
|
||
|
||
return {
|
||
showDetail, hideDetail,
|
||
showAgentCfg, sendAgentConfig, toggleCbox,
|
||
showSrvCfg, hideSrvCfg, saveSrvCfg, confirmDeleteAgent, doDeleteAgent,
|
||
showSmart, _copyInstallCmd,
|
||
};
|
||
})();
|