279 lines
13 KiB
HTML
Executable File
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>
|