Files
wevia-brain/s89-arsenal-screens/beast-monitor.html
2026-04-12 23:01:36 +02:00

279 lines
13 KiB
HTML
Executable File

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Beast Monitor | Arsenal WEVADS</title>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{
--bg:#060a14;--surface:#0c1220;--card:#111827;--border:#1e293b;
--cyan:#22d3ee;--cyan-dim:rgba(34,211,238,.12);
--green:#10b981;--green-dim:rgba(16,185,129,.12);
--red:#ef4444;--red-dim:rgba(239,68,68,.12);
--orange:#f59e0b;--orange-dim:rgba(245,158,11,.12);
--purple:#a78bfa;
--text:#e2e8f0;--dim:#64748b;--muted:#94a3b8;
--font:'DM Sans',sans-serif;--mono:'JetBrains Mono',monospace;
}
body{font-family:var(--font);background:var(--bg);color:var(--text);min-height:100vh;overflow-x:hidden}
.app{max-width:1440px;margin:0 auto;padding:24px}
/* Header */
.hdr{display:flex;align-items:center;justify-content:space-between;margin-bottom:28px;padding-bottom:16px;border-bottom:1px solid var(--border)}
.hdr h1{font-size:26px;font-weight:700}
.hdr h1 span{color:var(--cyan)}
.hdr-right{display:flex;align-items:center;gap:16px;font-family:var(--mono);font-size:13px}
.hdr-right .live{color:var(--green);display:flex;align-items:center;gap:6px}
.hdr-right .live::before{content:'';width:8px;height:8px;background:var(--green);border-radius:50%;box-shadow:0 0 8px var(--green);animation:pulse 2s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
/* Grid */
.grid{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:24px}
.grid-2{grid-template-columns:repeat(2,1fr)}
.grid-3{grid-template-columns:repeat(3,1fr)}
.span-2{grid-column:span 2}
/* Cards */
.card{background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:20px;position:relative;overflow:hidden}
.card-glow{box-shadow:0 0 30px rgba(34,211,238,.05)}
.card-title{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:1.2px;color:var(--dim);margin-bottom:14px;display:flex;align-items:center;gap:8px}
/* Service indicators */
.svc-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}
.svc{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:12px;text-align:center;transition:all .3s}
.svc.up{border-color:rgba(16,185,129,.3);background:var(--green-dim)}
.svc.down{border-color:rgba(239,68,68,.3);background:var(--red-dim);animation:alertPulse 1.5s infinite}
@keyframes alertPulse{0%,100%{opacity:1}50%{opacity:.7}}
.svc-name{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px}
.svc-status{font-size:20px}
.svc.up .svc-name{color:var(--green)}
.svc.down .svc-name{color:var(--red)}
/* Port grid */
.port-grid{display:grid;grid-template-columns:repeat(6,1fr);gap:8px}
.port{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:10px 8px;text-align:center;font-family:var(--mono);font-size:13px;font-weight:600;transition:all .3s}
.port.open{color:var(--green);border-color:rgba(16,185,129,.3)}
.port.closed{color:var(--red);border-color:rgba(239,68,68,.3)}
.port-label{font-size:9px;color:var(--dim);font-weight:400;margin-top:2px}
/* Gauge */
.gauge-wrap{display:flex;align-items:center;gap:16px;margin-bottom:14px}
.gauge{flex:1;height:10px;background:var(--card);border-radius:5px;overflow:hidden;position:relative}
.gauge-fill{height:100%;border-radius:5px;transition:width 1s ease}
.gauge-label{font-size:12px;font-weight:600;color:var(--muted);min-width:50px}
.gauge-val{font-family:var(--mono);font-size:14px;font-weight:700;min-width:55px;text-align:right}
/* Big number */
.big-num{font-family:var(--mono);font-size:36px;font-weight:700;line-height:1}
.big-label{font-size:12px;color:var(--dim);margin-top:6px}
/* ISP Table */
.isp-table{width:100%;border-collapse:separate;border-spacing:0}
.isp-table th{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--dim);padding:8px 10px;text-align:left;border-bottom:1px solid var(--border)}
.isp-table td{padding:8px 10px;font-size:13px;font-family:var(--mono);border-bottom:1px solid rgba(30,41,59,.4)}
.rate-bar{display:inline-block;height:6px;border-radius:3px;min-width:4px}
/* Load dots */
.load-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;text-align:center}
.load-val{font-family:var(--mono);font-size:22px;font-weight:700}
.load-label{font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.5px;margin-top:2px}
/* Refresh animation */
.refreshing .card{opacity:.7;transition:opacity .2s}
/* Responsive */
@media(max-width:1024px){.grid{grid-template-columns:repeat(2,1fr)}.svc-grid{grid-template-columns:repeat(2,1fr)}.port-grid{grid-template-columns:repeat(3,1fr)}}
@media(max-width:600px){.grid{grid-template-columns:1fr}.grid-2,.grid-3{grid-template-columns:1fr}}
</style>
</head>
<body>
<div class="app" id="app">
<div class="hdr">
<h1>🦁 <span>Beast</span> Monitor</h1>
<div class="hdr-right">
<span class="live">LIVE</span>
<span id="clock" style="color:var(--cyan)">--:--:--</span>
<span id="refreshLabel" style="color:var(--dim)">⟳ 10s</span>
</div>
</div>
<!-- Row 1: Services + Ports -->
<div class="grid grid-2" style="margin-bottom:16px">
<div class="card">
<div class="card-title">🔌 Services</div>
<div class="svc-grid" id="services">
<div class="svc"><div class="svc-name">PMTA</div><div class="svc-status"></div></div>
<div class="svc"><div class="svc-name">Apache</div><div class="svc-status"></div></div>
<div class="svc"><div class="svc-name">PgSQL</div><div class="svc-status"></div></div>
<div class="svc"><div class="svc-name">Redis</div><div class="svc-status"></div></div>
<div class="svc"><div class="svc-name">Postfix</div><div class="svc-status"></div></div>
</div>
</div>
<div class="card">
<div class="card-title">🔓 Ports</div>
<div class="port-grid" id="ports">
<div class="port">25<div class="port-label">SMTP</div></div>
<div class="port">2526<div class="port-label">PMTA Mgmt</div></div>
<div class="port">5821<div class="port-label">WEVADS</div></div>
<div class="port">5432<div class="port-label">PgSQL</div></div>
<div class="port">6379<div class="port-label">Redis</div></div>
<div class="port">8080<div class="port-label">Proxy</div></div>
</div>
</div>
</div>
<!-- Row 2: System Gauges + Big Numbers -->
<div class="grid" style="margin-bottom:16px">
<div class="card span-2">
<div class="card-title">💾 System Resources</div>
<div class="gauge-wrap">
<span class="gauge-label">DISK</span>
<div class="gauge"><div class="gauge-fill" id="diskBar" style="width:0;background:var(--cyan)"></div></div>
<span class="gauge-val" id="diskVal">—%</span>
</div>
<div class="gauge-wrap">
<span class="gauge-label">RAM</span>
<div class="gauge"><div class="gauge-fill" id="ramBar" style="width:0;background:var(--purple)"></div></div>
<span class="gauge-val" id="ramVal">—%</span>
</div>
<div style="margin-top:16px">
<div class="card-title" style="margin-bottom:10px">📈 Load Average</div>
<div class="load-grid" id="loadGrid">
<div><div class="load-val"></div><div class="load-label">1 min</div></div>
<div><div class="load-val"></div><div class="load-label">5 min</div></div>
<div><div class="load-val"></div><div class="load-label">15 min</div></div>
</div>
</div>
</div>
<div class="card" style="display:flex;flex-direction:column;align-items:center;justify-content:center">
<div class="big-num" id="sendsToday" style="color:var(--cyan)"></div>
<div class="big-label">Sends Today</div>
</div>
<div class="card" style="display:flex;flex-direction:column;align-items:center;justify-content:center">
<div class="big-num" id="pmtaQueue" style="color:var(--orange)"></div>
<div class="big-label">PMTA Queue</div>
</div>
</div>
<!-- Row 3: Brain + ISPs -->
<div class="grid grid-2">
<div class="card">
<div class="card-title">🧠 Brain Engine</div>
<div style="display:flex;gap:24px;align-items:center">
<div style="text-align:center">
<div class="big-num" id="brainTotal" style="color:var(--cyan);font-size:28px"></div>
<div class="big-label">Total Configs</div>
</div>
<div style="text-align:center">
<div class="big-num" id="brainWinners" style="color:var(--green);font-size:28px"></div>
<div class="big-label">Winners 🏆</div>
</div>
<div style="text-align:center">
<div class="big-num" id="brainRate" style="font-size:22px"></div>
<div class="big-label">Win Rate</div>
</div>
</div>
</div>
<div class="card">
<div class="card-title">🎯 Top ISPs (by inbox rate)</div>
<table class="isp-table">
<thead><tr><th>ISP</th><th>Configs</th><th>Avg Rate</th><th></th></tr></thead>
<tbody id="ispBody">
<tr><td colspan="4" style="text-align:center;color:var(--dim)">Loading...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
const API = '/api/monitor.php';
async function refresh() {
try {
const r = await fetch(API + '?t=' + Date.now());
const d = await r.json();
if (d.error) return;
renderServices(d.services || []);
renderPorts(d.ports || []);
renderSystem(d.system || {});
renderBrain(d.brain || {});
renderISPs(d.top_isps || []);
document.getElementById('sendsToday').textContent = d.sends_today ?? '—';
document.getElementById('pmtaQueue').textContent = d.pmta_queue ?? '—';
} catch(e) {
console.error('Monitor refresh failed:', e);
}
}
function renderServices(svcs) {
const el = document.getElementById('services');
const names = {'pmta':'PMTA','apache':'Apache','postgresql':'PgSQL','redis':'Redis','postfix':'Postfix'};
el.innerHTML = svcs.map(s => {
const up = s.status === 'running';
return '<div class="svc ' + (up ? 'up' : 'down') + '"><div class="svc-name">' + (names[s.name] || s.name) + '</div><div class="svc-status">' + (up ? '✅' : '❌') + '</div></div>';
}).join('');
}
function renderPorts(ports) {
const labels = {25:'SMTP',2526:'PMTA Mgmt',5821:'WEVADS',5432:'PgSQL',6379:'Redis',8080:'Proxy'};
const el = document.getElementById('ports');
el.innerHTML = ports.map(p => {
return '<div class="port ' + (p.open ? 'open' : 'closed') + '">' + p.port + '<div class="port-label">' + (labels[p.port] || '') + '</div></div>';
}).join('');
}
function renderSystem(sys) {
const diskPct = sys.disk_pct || 0;
const ramPct = sys.ram_pct || 0;
document.getElementById('diskBar').style.width = diskPct + '%';
document.getElementById('diskBar').style.background = diskPct > 85 ? 'var(--red)' : diskPct > 70 ? 'var(--orange)' : 'var(--cyan)';
document.getElementById('diskVal').textContent = diskPct + '% (' + (sys.disk_free_gb || '?') + 'GB free)';
document.getElementById('ramBar').style.width = ramPct + '%';
document.getElementById('ramBar').style.background = ramPct > 85 ? 'var(--red)' : ramPct > 70 ? 'var(--orange)' : 'var(--purple)';
document.getElementById('ramVal').textContent = ramPct + '% (' + (sys.ram_used_mb || '?') + '/' + (sys.ram_total_mb || '?') + ' MB)';
const load = sys.load || [0,0,0];
const labels = ['1 min','5 min','15 min'];
document.getElementById('loadGrid').innerHTML = load.map((v, i) => {
const color = v > 4 ? 'var(--red)' : v > 2 ? 'var(--orange)' : 'var(--green)';
return '<div><div class="load-val" style="color:' + color + '">' + v.toFixed(2) + '</div><div class="load-label">' + labels[i] + '</div></div>';
}).join('');
}
function renderBrain(brain) {
const total = brain.total_configs || 0;
const winners = brain.winners || 0;
document.getElementById('brainTotal').textContent = total;
document.getElementById('brainWinners').textContent = winners;
document.getElementById('brainRate').textContent = total > 0 ? Math.round(winners/total*100) + '%' : '—';
}
function renderISPs(isps) {
const el = document.getElementById('ispBody');
if (!isps.length) { el.innerHTML = '<tr><td colspan="4" style="text-align:center;color:var(--dim)">No data</td></tr>'; return; }
el.innerHTML = isps.map(row => {
const rate = parseFloat(row.avg_rate || 0);
const color = rate >= 85 ? 'var(--green)' : rate >= 65 ? 'var(--orange)' : 'var(--red)';
return '<tr><td style="font-weight:600">' + esc(row.isp_target) + '</td><td style="color:var(--muted)">' + row.cnt + '</td><td style="color:' + color + ';font-family:var(--mono);font-weight:600">' + rate.toFixed(1) + '%</td><td><div class="rate-bar" style="width:' + rate + '%;background:' + color + '"></div></td></tr>';
}).join('');
}
function esc(s) { if (!s) return ''; const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
// Clock
setInterval(() => { document.getElementById('clock').textContent = new Date().toLocaleTimeString('fr-FR'); }, 1000);
// Auto refresh
refresh();
setInterval(refresh, 10000);
</script>
</body>
</html>