f69c22039b
- nginx: client_max_body_size 10m (limite par défaut 1 Mo bloquait les images) - icons.go: import _ golang.org/x/image/webp et image/gif pour décoder WEBP/GIF - index.html: retire SVG de l'accept (serveur le rejette) et corrige le hint - popups.js: try/catch autour de uploadIcon → message d'erreur visible dans le hint pendant 4s si l'upload échoue ; reset du file input pour re-sélectionner le même fichier ; rafraîchit l'img de la tuile avec cache-busting après succès Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
460 lines
28 KiB
JavaScript
460 lines
28 KiB
JavaScript
const Popups = (() => {
|
||
let _currentAgentId = null;
|
||
let _agentCfgData = null;
|
||
let _resizeObs = 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);
|
||
|
||
const smartBtn = metrics?.smart
|
||
? `<div class="smart-btn ok" onclick="Popups.showSmart('${esc(agentId)}')" data-tip="Voir la santé complète du disque">
|
||
<div class="smart-dot"></div>
|
||
<span style="font-weight:600">SMART</span>
|
||
<span>·</span>
|
||
<span>${metrics.smart.passed ? 'PASSED' : 'FAILED'}</span>
|
||
${metrics.smart.temperature ? `<span style="font-family:var(--font-mono);font-size:10px;color:var(--ink-3)"><i class="fa-solid fa-temperature-half"></i> ${metrics.smart.temperature}°C</span>` : ''}
|
||
<i class="fa-solid fa-chevron-right" style="font-size:10px;color:var(--ink-4);margin-left:auto"></i>
|
||
</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>
|
||
<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>
|
||
</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>
|
||
</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>
|
||
${smartBtn}
|
||
</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(() => {
|
||
API.putServerConfig({
|
||
...App.serverConfig,
|
||
popup_detail_w: pd.offsetWidth,
|
||
popup_detail_h: pd.offsetHeight,
|
||
}).catch(() => {});
|
||
});
|
||
_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>
|
||
<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">
|
||
<label style="font-size:0.85em;color:var(--fg2)">Commande curl — copiez et lancez en root sur la machine cible</label>
|
||
<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="navigator.clipboard.writeText(document.getElementById('s-install-cmd').value).then(()=>{this.textContent='✓ Copié';setTimeout(()=>this.textContent='Copier',1500)})">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),
|
||
};
|
||
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';
|
||
hideSrvCfg();
|
||
}
|
||
|
||
// ══ POPUP SMART ══
|
||
function showSmart(agentId) {
|
||
const m = Grid.getAgent(agentId)?.metrics?.smart;
|
||
if (!m) return;
|
||
document.getElementById('smart-sub').textContent = agentId;
|
||
const passColor = m.passed ? 'var(--ok)' : 'var(--err)';
|
||
const passText = m.passed ? 'Disque en bonne santé' : 'Disque en mauvais état';
|
||
const passSub = m.passed
|
||
? 'Aucun problème détecté. Le disque fonctionne normalement.'
|
||
: 'Des problèmes ont été détectés. Envisagez un remplacement.';
|
||
|
||
document.getElementById('smart-body').innerHTML = `
|
||
<div class="smart-verdict" style="${m.passed ? '' : 'background:rgba(251,73,52,.1);border-color:rgba(251,73,52,.3)'}">
|
||
<div style="font-size:28px;color:${passColor}"><i class="fa-solid ${m.passed ? 'fa-circle-check' : 'fa-circle-xmark'}"></i></div>
|
||
<div><div style="font-size:16px;font-weight:700;color:${passColor}">${passText}</div>
|
||
<div style="font-size:12px;color:var(--ink-3);margin-top:3px">${passSub}</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:var(--warn);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:rgba(77,187,38,.15);color:var(--ok)">Normale</span>
|
||
</div>
|
||
<div class="si-val">${m.temperature}<span class="u">°C</span></div>
|
||
<div class="si-desc">Idéal : 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:${m.reallocated_sectors > 0 ? 'var(--err)' : 'var(--ok)'};font-size:14px;width:22px;text-align:center"><i class="fa-solid fa-circle-check"></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">S'ils apparaissent en grand nombre, une panne est imminente.</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:var(--blue);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">Heures 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. Un disque dure en moyenne 3 à 5 ans.</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:var(--ok);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</span>
|
||
</div>
|
||
<div class="si-val">${m.wear_level}<span class="u">%</span></div>
|
||
<div class="si-desc">100% = neuf · 0% = fin de vie recommandée.</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);
|
||
}
|
||
}
|
||
|
||
return {
|
||
showDetail, hideDetail,
|
||
showAgentCfg, sendAgentConfig, toggleCbox,
|
||
showSrvCfg, hideSrvCfg, saveSrvCfg, confirmDeleteAgent, doDeleteAgent,
|
||
showSmart,
|
||
};
|
||
})();
|