Standalone dashboard reading from Oktopus MongoDB. Displays: serial, MAC, WAN IP, firmware, uptime, WiFi signal/clients, CPU/RAM usage with visual bars. Auto-refresh 30s. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
150 lines
7.9 KiB
HTML
150 lines
7.9 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Targo — Device Monitor</title>
|
|
<style>
|
|
* { margin:0; padding:0; box-sizing:border-box; }
|
|
body { font-family:'Inter',system-ui,sans-serif; background:#f4f6f9; color:#1e1e2a; }
|
|
.header { background:#fff; border-bottom:1px solid #e0e3e8; padding:12px 24px; display:flex; align-items:center; gap:16px; }
|
|
.header h1 { font-size:1.1rem; font-weight:700; color:#0ea550; }
|
|
.header .count { background:#0ea550; color:#fff; font-size:0.7rem; font-weight:700; padding:2px 8px; border-radius:10px; }
|
|
.header .status-ok { color:#0ea550; font-size:0.75rem; }
|
|
.header .status-warn { color:#e4a944; font-size:0.75rem; }
|
|
.grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(340px,1fr)); gap:16px; padding:20px 24px; }
|
|
.card { background:#fff; border-radius:12px; border:1px solid #e8eaef; overflow:hidden; transition:box-shadow 0.15s; }
|
|
.card:hover { box-shadow:0 4px 20px rgba(0,0,0,0.08); }
|
|
.card-hdr { display:flex; align-items:center; gap:10px; padding:14px 16px; border-bottom:1px solid #f0f1f4; }
|
|
.card-hdr .dot { width:10px; height:10px; border-radius:50%; flex-shrink:0; }
|
|
.dot-online { background:#0ea550; box-shadow:0 0 6px rgba(14,165,80,0.4); }
|
|
.dot-offline { background:#e6364b; box-shadow:0 0 6px rgba(230,54,75,0.3); }
|
|
.card-hdr .alias { font-weight:700; font-size:0.85rem; flex:1; }
|
|
.card-hdr .model { font-size:0.7rem; color:#888; }
|
|
.card-body { padding:12px 16px; display:grid; grid-template-columns:1fr 1fr; gap:6px 16px; }
|
|
.field { display:flex; flex-direction:column; }
|
|
.field-label { font-size:0.6rem; font-weight:700; text-transform:uppercase; letter-spacing:0.04em; color:#999; }
|
|
.field-value { font-size:0.8rem; font-weight:600; font-variant-numeric:tabular-nums; }
|
|
.field-value.mono { font-family:'Roboto Mono',monospace; font-size:0.75rem; }
|
|
.wifi-section { grid-column:1/-1; display:flex; gap:12px; margin-top:4px; padding-top:8px; border-top:1px solid #f0f1f4; }
|
|
.wifi-band { flex:1; background:#f8f9fb; border-radius:8px; padding:8px 10px; }
|
|
.wifi-band-title { font-size:0.6rem; font-weight:700; text-transform:uppercase; color:#0ea550; margin-bottom:4px; }
|
|
.wifi-row { display:flex; justify-content:space-between; font-size:0.72rem; }
|
|
.wifi-row .wlabel { color:#888; }
|
|
.wifi-row .wval { font-weight:600; }
|
|
.signal-bar { display:flex; gap:2px; align-items:flex-end; height:14px; }
|
|
.signal-bar span { width:4px; border-radius:1px; background:#ddd; }
|
|
.signal-bar span.active { background:#0ea550; }
|
|
.signal-bar span:nth-child(1) { height:4px; }
|
|
.signal-bar span:nth-child(2) { height:7px; }
|
|
.signal-bar span:nth-child(3) { height:10px; }
|
|
.signal-bar span:nth-child(4) { height:14px; }
|
|
.card-footer { padding:8px 16px; background:#fafbfc; border-top:1px solid #f0f1f4; display:flex; gap:12px; align-items:center; }
|
|
.usage-bar { flex:1; height:4px; background:#eee; border-radius:2px; overflow:hidden; }
|
|
.usage-fill { height:100%; border-radius:2px; transition:width 0.3s; }
|
|
.usage-label { font-size:0.6rem; font-weight:600; color:#888; min-width:50px; }
|
|
.uptime { font-size:0.7rem; color:#0ea550; font-weight:600; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>📡 Targo Device Monitor</h1>
|
|
<span class="count" id="device-count">0</span>
|
|
<span class="status-ok" id="online-count"></span>
|
|
<span class="status-warn" id="offline-count"></span>
|
|
<div style="flex:1"></div>
|
|
<span style="font-size:0.7rem;color:#999" id="last-refresh"></span>
|
|
</div>
|
|
<div class="grid" id="grid"></div>
|
|
|
|
<script>
|
|
function signalBars(dbm) {
|
|
const s = Math.abs(dbm);
|
|
const level = s < 45 ? 4 : s < 55 ? 3 : s < 65 ? 2 : s < 75 ? 1 : 0;
|
|
return `<div class="signal-bar">${[1,2,3,4].map(i => `<span class="${i<=level?'active':''}"></span>`).join('')}</div>`;
|
|
}
|
|
|
|
function fmtUptime(sec) {
|
|
if (!sec) return 'Offline';
|
|
const d = Math.floor(sec/86400), h = Math.floor((sec%86400)/3600);
|
|
return d > 0 ? `${d}j ${h}h` : `${h}h`;
|
|
}
|
|
|
|
function fmtDate(d) {
|
|
if (!d) return '—';
|
|
const dt = new Date(d);
|
|
const diff = Date.now() - dt.getTime();
|
|
if (diff < 60000) return 'À l\'instant';
|
|
if (diff < 3600000) return `Il y a ${Math.floor(diff/60000)}min`;
|
|
if (diff < 86400000) return `Il y a ${Math.floor(diff/3600000)}h`;
|
|
return dt.toLocaleDateString('fr-CA');
|
|
}
|
|
|
|
function usageColor(pct) {
|
|
if (pct < 50) return '#0ea550';
|
|
if (pct < 80) return '#e4a944';
|
|
return '#e6364b';
|
|
}
|
|
|
|
function renderDevice(d) {
|
|
const online = d.status === 1;
|
|
const wifi24 = d.wifi_24 || {};
|
|
const wifi5 = d.wifi_5 || null;
|
|
return `
|
|
<div class="card">
|
|
<div class="card-hdr">
|
|
<div class="dot ${online ? 'dot-online' : 'dot-offline'}"></div>
|
|
<span class="alias">${d.alias || d.sn}</span>
|
|
<span class="model">${d.vendor} ${d.model}</span>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="field"><span class="field-label">Serial</span><span class="field-value mono">${d.sn}</span></div>
|
|
<div class="field"><span class="field-label">MAC</span><span class="field-value mono">${d.mac || '—'}</span></div>
|
|
<div class="field"><span class="field-label">WAN IP</span><span class="field-value mono">${d.wan_ip || '—'}</span></div>
|
|
<div class="field"><span class="field-label">Firmware</span><span class="field-value">${d.firmware || d.version}</span></div>
|
|
<div class="field"><span class="field-label">Uptime</span><span class="field-value uptime">${fmtUptime(d.uptime)}</span></div>
|
|
<div class="field"><span class="field-label">Dernière vue</span><span class="field-value">${fmtDate(d.last_seen)}</span></div>
|
|
<div class="wifi-section">
|
|
<div class="wifi-band">
|
|
<div class="wifi-band-title">2.4 GHz</div>
|
|
<div class="wifi-row"><span class="wlabel">SSID</span><span class="wval">${wifi24.ssid || '—'}</span></div>
|
|
<div class="wifi-row"><span class="wlabel">Signal</span><span class="wval">${wifi24.signal ? wifi24.signal + 'dBm ' + signalBars(wifi24.signal) : '—'}</span></div>
|
|
<div class="wifi-row"><span class="wlabel">Ch</span><span class="wval">${wifi24.channel || '—'}</span></div>
|
|
<div class="wifi-row"><span class="wlabel">Clients</span><span class="wval">${wifi24.clients ?? '—'}</span></div>
|
|
</div>
|
|
${wifi5 ? `<div class="wifi-band">
|
|
<div class="wifi-band-title">5 GHz</div>
|
|
<div class="wifi-row"><span class="wlabel">SSID</span><span class="wval">${wifi5.ssid}</span></div>
|
|
<div class="wifi-row"><span class="wlabel">Signal</span><span class="wval">${wifi5.signal}dBm ${signalBars(wifi5.signal)}</span></div>
|
|
<div class="wifi-row"><span class="wlabel">Ch</span><span class="wval">${wifi5.channel}</span></div>
|
|
<div class="wifi-row"><span class="wlabel">Clients</span><span class="wval">${wifi5.clients}</span></div>
|
|
</div>` : ''}
|
|
</div>
|
|
</div>
|
|
<div class="card-footer">
|
|
<span class="usage-label">CPU ${d.cpu_usage || 0}%</span>
|
|
<div class="usage-bar"><div class="usage-fill" style="width:${d.cpu_usage||0}%;background:${usageColor(d.cpu_usage||0)}"></div></div>
|
|
<span class="usage-label">RAM ${d.memory_usage || 0}%</span>
|
|
<div class="usage-bar"><div class="usage-fill" style="width:${d.memory_usage||0}%;background:${usageColor(d.memory_usage||0)}"></div></div>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
async function loadDevices() {
|
|
try {
|
|
const res = await fetch('/api/monitor/devices');
|
|
const devices = await res.json();
|
|
document.getElementById('grid').innerHTML = devices.map(renderDevice).join('');
|
|
document.getElementById('device-count').textContent = devices.length;
|
|
const online = devices.filter(d => d.status === 1).length;
|
|
document.getElementById('online-count').textContent = `● ${online} en ligne`;
|
|
document.getElementById('offline-count').textContent = `● ${devices.length - online} hors ligne`;
|
|
document.getElementById('last-refresh').textContent = new Date().toLocaleTimeString('fr-CA');
|
|
} catch(e) { console.error(e); }
|
|
}
|
|
loadDevices();
|
|
setInterval(loadDevices, 30000);
|
|
</script>
|
|
</body>
|
|
</html>
|