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

157 lines
15 KiB
HTML
Executable File

<!DOCTYPE html><html lang="fr"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>WEVADS • Performance Dashboard</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>
:root{--bg:#060a14;--s:#0c1220;--s2:#111827;--b:#1e293b;--t:#e2e8f0;--d:#64748b;--cy:#22d3ee;--gn:#34d399;--am:#fbbf24;--rd:#f87171;--pu:#a78bfa;--bl:#60a5fa;--pk:#f472b6;--og:#fb923c}
.light{--bg:#f0f2f5;--s:#ffffff;--s2:#f8fafc;--b:#e2e8f0;--t:#1e293b;--d:#64748b}
*{margin:0;padding:0;box-sizing:border-box}body{background:var(--bg);color:var(--t);font-family:'DM Sans',sans-serif;font-size:11px}
.hdr{background:var(--s);border-bottom:1px solid var(--b);padding:12px 20px;display:flex;align-items:center;justify-content:space-between}.hdr h1{font-size:16px;font-weight:700}.hdr h1 span{color:var(--cy)}
.wrap{padding:16px;max-width:1400px;margin:0 auto}
.stats{display:grid;grid-template-columns:repeat(6,1fr);gap:10px;margin-bottom:16px}
.sc{background:var(--s);border:1px solid var(--b);border-radius:10px;padding:14px;text-align:center;cursor:pointer;transition:.2s}.sc:hover{border-color:var(--cy)}
.sc .n{font-family:'JetBrains Mono',monospace;font-size:22px;font-weight:700}.sc .l{font-size:9px;text-transform:uppercase;color:var(--d);margin-top:4px}.sc .sub{font-size:8px;color:var(--d);margin-top:2px}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:12px}.grid3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px}
.card{background:var(--s);border:1px solid var(--b);border-radius:10px;padding:16px;margin-bottom:12px}
.card h3{font-size:12px;font-weight:700;margin-bottom:10px}
table{width:100%;border-collapse:collapse;font-size:10px}th{text-align:left;color:var(--d);text-transform:uppercase;font-size:9px;padding:6px 8px;border-bottom:1px solid var(--b)}td{padding:6px 8px;border-bottom:1px solid rgba(30,41,59,.3)}
.badge{font-size:8px;padding:2px 6px;border-radius:3px;font-weight:600}.badge-gn{background:rgba(52,211,153,.15);color:var(--gn)}.badge-am{background:rgba(251,191,36,.15);color:var(--am)}.badge-rd{background:rgba(248,113,113,.15);color:var(--rd)}
.progress{height:6px;background:var(--s2);border-radius:3px;margin-top:3px}.progress-fill{height:100%;border-radius:3px;transition:width .5s}
.bar-chart{display:flex;gap:3px;align-items:flex-end;height:100px}
.bar{flex:1;border-radius:3px 3px 0 0;transition:height .5s;min-width:20px;position:relative;cursor:pointer}
.bar:hover::after{content:attr(data-label);position:absolute;top:-18px;left:50%;transform:translateX(-50%);font-size:8px;color:var(--t);background:var(--s);padding:2px 4px;border-radius:3px;white-space:nowrap}
.dot{width:8px;height:8px;border-radius:50%;display:inline-block;margin-right:4px}.dot-gn{background:var(--gn);box-shadow:0 0 6px var(--gn)}.dot-am{background:var(--am)}.dot-rd{background:var(--rd)}
.loading{color:var(--d);font-style:italic;text-align:center;padding:20px}
.drill-link{color:var(--cy);cursor:pointer;text-decoration:underline;font-size:9px}
.drill-link:hover{color:var(--bl)}
.nav-pills{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px}
.nav-pill{background:var(--s2);border:1px solid var(--b);border-radius:6px;padding:4px 10px;font-size:9px;color:var(--d);text-decoration:none;transition:.2s;cursor:pointer}.nav-pill:hover{border-color:var(--cy);color:var(--t)}.nav-pill.active{border-color:var(--cy);color:var(--cy)}
.toggle-btn{position:fixed;top:10px;right:56px;z-index:9999;background:rgba(17,24,39,.9);border:1px solid #1e293b;border-radius:50%;width:30px;height:30px;display:flex;align-items:center;justify-content:center;padding:0;cursor:pointer;font-size:13px;color:#e2e8f0;backdrop-filter:blur(8px)}
.live-dot{display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--gn);margin-right:4px;animation:blink 2s infinite}
@keyframes blink{0%,100%{opacity:1}50%{opacity:.3}}
@media(max-width:900px){.stats{grid-template-columns:repeat(3,1fr)}.grid2,.grid3{grid-template-columns:1fr}}
</style>
</head><body>
<!-- nav-pills-bar -->
<div class="hdr"><div><h1>📊 WEVADS • <span>Performance Dashboard</span></h1><span style="font-size:10px;color:var(--d)"><span class="live-dot"></span>LIVE — Données PostgreSQL temps réel — Auto-refresh 30s</span></div><div style="display:flex;gap:8px;align-items:center"><span id="db-status" class="badge badge-gn">● DB CONNECTED</span><span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--d)" id="clock"></span></div></div>
<div class="wrap">
<div class="nav-pills">
<a href="bpms-command-center.html" class="nav-pill">🚀 BPMS</a>
<a href="brain-unified-send.html" class="nav-pill">🧠 Brain Send</a>
<a href="brain-drilldown.html" class="nav-pill">📊 Drilldown</a>
<a href="performance-dashboard.html" class="nav-pill active">📈 Performance</a>
<a href="neural-dom-mutator.html" class="nav-pill">🧬 DOM Mutator</a>
<a href="trap-detector.html" class="nav-pill">🛡️ Traps</a>
<a href="fingerprint-sync.html" class="nav-pill">👤 Fingerprint</a>
<a href="predictive-send-window.html" class="nav-pill">📈 Predictive</a>
<a href="auto-healing-rotation.html" class="nav-pill">🔄 Auto-Heal</a>
<a href="harvest-manager.html" class="nav-pill">🌾 Harvest</a>
<a href="data-manager.html" class="nav-pill">📦 Data</a>
<a href="health.html" class="nav-pill">💊 Health</a>
<a href="brain-report.html" class="nav-pill">📋 Report</a>
</div>
<div class="stats" id="top-stats">
<div class="sc" onclick="drillDown('sends')"><div class="n" style="color:var(--cy)" id="s-today"></div><div class="l">Emails aujourd'hui</div><div class="sub drill-link">→ Drill par ISP</div></div>
<div class="sc" onclick="drillDown('inbox')"><div class="n" style="color:var(--gn)" id="s-inbox"></div><div class="l">Inbox Rate</div><div class="sub drill-link">→ Par méthode</div></div>
<div class="sc" onclick="drillDown('bounce')"><div class="n" style="color:var(--am)" id="s-bounce"></div><div class="l">Bounce Rate</div><div class="sub drill-link">→ Par domaine</div></div>
<div class="sc"><div class="n" style="color:var(--pu)" id="s-accounts"></div><div class="l">Comptes actifs</div><div class="sub">O365 + GSuite</div></div>
<div class="sc"><div class="n" style="color:var(--og)" id="s-cpu"></div><div class="l">CPU</div><div class="sub" id="s-ram">RAM: —</div></div>
<div class="sc"><div class="n" style="color:var(--pk)" id="s-pmta"></div><div class="l">PMTA Queue</div><div class="sub" id="s-pmta-status"></div></div>
</div>
<!-- DRILL DOWN PANEL -->
<div class="card" id="drill-panel" style="display:none">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
<h3 id="drill-title" style="color:var(--cy)">🔍 Drill Down</h3>
<button onclick="closeDrill()" style="background:none;border:1px solid var(--b);border-radius:4px;color:var(--d);padding:4px 8px;cursor:pointer;font-size:10px">✕ Fermer</button>
</div>
<div id="drill-content"><div class="loading">Chargement...</div></div>
</div>
<div class="grid2">
<div class="card">
<h3 style="color:var(--cy)">📧 Performance par ISP — <span style="font-size:9px;color:var(--d)">Derniers 7 jours</span></h3>
<table id="isp-table"><thead><tr><th>ISP</th><th>Volume</th><th>Inbox %</th><th>Bar</th><th>Drill</th></tr></thead><tbody id="isp-body"><tr><td colspan="5" class="loading">Chargement données PostgreSQL...</td></tr></tbody></table>
</div>
<div class="card">
<h3 style="color:var(--gn)">🖥️ Santé Système — <span style="font-size:9px;color:var(--d)">Live</span></h3>
<div id="system-health"><div class="loading">Chargement...</div></div>
</div>
</div>
<div class="grid2">
<div class="card">
<h3>💾 Tables PostgreSQL — <span style="font-size:9px;color:var(--d)">adx_system</span></h3>
<table><thead><tr><th>Table</th><th>Rows</th><th>Size</th></tr></thead><tbody id="db-tables"><tr><td colspan="3" class="loading">Chargement...</td></tr></tbody></table>
</div>
<div class="card">
<h3>🌐 Reputation IPs</h3>
<div id="rep-data"><div class="loading">Chargement...</div></div>
</div>
</div>
</div>
<script>
var API='/api/arsenal-data.php';
async function fetchAPI(action){try{var r=await fetch(API+'?action='+action);return await r.json()}catch(e){return{error:e.message}}}
async function loadAll(){
// Send stats
var ss=await fetchAPI('send_stats');
if(ss.today){
var t=parseInt(ss.today.total)||0;var d=parseInt(ss.today.delivered)||0;var b=parseInt(ss.today.bounced)||0;
document.getElementById('s-today').textContent=t.toLocaleString('fr-FR');
document.getElementById('s-inbox').textContent=t>0?Math.round(d/t*100)+'%':'—';
document.getElementById('s-bounce').textContent=t>0?(b/t*100).toFixed(1)+'%':'—';
}
// ISP table
if(ss.by_isp&&ss.by_isp.length>0){
var h='';ss.by_isp.forEach(function(r){
var pct=parseFloat(r.inbox_rate)||0;var c=pct>=90?'var(--gn)':pct>=70?'var(--am)':'var(--rd)';
h+='<tr><td style="font-weight:600">'+r.isp+'</td><td>'+parseInt(r.cnt).toLocaleString()+'</td><td style="color:'+c+';font-weight:700">'+pct+'%</td><td><div class="progress" style="width:80px"><div class="progress-fill" style="width:'+pct+'%;background:'+c+'"></div></div></td><td><span class="drill-link" onclick="drillISP(\''+r.isp+'\')">→ Drill</span></td></tr>';
});document.getElementById('isp-body').innerHTML=h;
} else { document.getElementById('isp-body').innerHTML='<tr><td colspan="5" style="color:var(--am)">Aucune donnée send_log récente</td></tr>'; }
// Accounts
var ac=await fetchAPI('accounts');
if(ac.total)document.getElementById('s-accounts').textContent=ac.active_count||ac.total;
// System
var sy=await fetchAPI('system');
if(sy.cpu_pct!==undefined){
document.getElementById('s-cpu').textContent=sy.cpu_pct+'%';
document.getElementById('s-ram').textContent='RAM: '+sy.mem_used_mb+'MB/'+sy.mem_total_mb+'MB';
var sh='';
if(sy.services){Object.keys(sy.services).forEach(function(k){var v=sy.services[k];var ok=v==='active';sh+='<div style="display:flex;justify-content:space-between;padding:4px 0;border-bottom:1px solid rgba(30,41,59,.3);font-size:10px"><span><span class="dot '+(ok?'dot-gn':'dot-rd')+'"></span>'+k+'</span><span class="badge '+(ok?'badge-gn':'badge-rd')+'">'+(ok?'RUNNING':'DOWN')+'</span></div>'})}
sh+='<div style="margin-top:8px;font-size:9px;color:var(--d)">CPU: '+sy.cpu_pct+'% • RAM: '+sy.mem_pct+'% • Disk: '+sy.disk_pct+'</div>';
sh+='<div style="font-size:9px;color:var(--d)">Load: '+sy.load+'</div>';
document.getElementById('system-health').innerHTML=sh;
}
// PMTA
var pm=await fetchAPI('pmta_status');
if(pm.status){document.getElementById('s-pmta').textContent=pm.queue_recipients||'0';document.getElementById('s-pmta-status').textContent=pm.status==='active'?'RUNNING':'DOWN'}
// DB tables
var dt=await fetchAPI('db_tables');
if(Array.isArray(dt)&&dt.length>0){var h='';dt.forEach(function(r){h+='<tr><td style="font-family:JetBrains Mono,monospace;font-size:9px">'+r.table_name+'</td><td>'+parseInt(r.row_count).toLocaleString()+'</td><td>'+r.size+'</td></tr>'});document.getElementById('db-tables').innerHTML=h}
// Reputation
var rp=await fetchAPI('reputation');
if(Array.isArray(rp)){var h='';rp.forEach(function(r){h+='<div style="display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid rgba(30,41,59,.3);font-size:10px"><span style="font-family:JetBrains Mono,monospace">'+r.ip+'</span><span class="badge '+(r.status==='clean'?'badge-gn':'badge-rd')+'">'+r.status.toUpperCase()+'</span></div>'});document.getElementById('rep-data').innerHTML=h}
// DB status
var st=await fetchAPI('status');
document.getElementById('db-status').className='badge '+(st.db_adx==='connected'?'badge-gn':'badge-rd');
document.getElementById('db-status').textContent=(st.db_adx==='connected'?'● ':'✕ ')+'DB: '+st.db_adx;
}
function drillDown(type){
var p=document.getElementById('drill-panel');p.style.display='block';
var t=document.getElementById('drill-title');var c=document.getElementById('drill-content');
if(type==='sends'){t.textContent='🔍 Drill: Envois par ISP → Par heure → Par compte';c.innerHTML='<div class="loading">Chargement drill level 1...</div>';fetchAPI('send_stats').then(function(d){if(d.by_isp){var h='<table><thead><tr><th>ISP</th><th>Volume 7j</th><th>Inbox</th><th>Cliquer pour Level 2</th></tr></thead><tbody>';d.by_isp.forEach(function(r){h+='<tr><td>'+r.isp+'</td><td>'+parseInt(r.cnt).toLocaleString()+'</td><td>'+(r.inbox_rate||'—')+'%</td><td><span class="drill-link" onclick="drillISP(\''+r.isp+'\')">→ Par heure / Par compte</span></td></tr>'});h+='</tbody></table>';c.innerHTML=h}});}
else if(type==='inbox'){t.textContent='🔍 Drill: Inbox par méthode → Par config → Par template';c.innerHTML='<table><thead><tr><th>Méthode</th><th>Volume</th><th>Inbox</th><th>Level 3</th></tr></thead><tbody><tr><td>O365 Graph API</td><td>—</td><td>—</td><td><span class="drill-link">→ Par config</span></td></tr><tr><td>PMTA Exchange</td><td>—</td><td>—</td><td><span class="drill-link">→ Par VMTA</span></td></tr><tr><td>GSuite SMTP</td><td>—</td><td>—</td><td><span class="drill-link">→ Par compte</span></td></tr></tbody></table><div style="font-size:9px;color:var(--d);margin-top:8px">💡 Les données niveau 2-3 nécessitent send_log détaillé</div>';}
else if(type==='bounce'){t.textContent='🔍 Drill: Bounces → Par domaine → Par raison';c.innerHTML='<div style="font-size:10px;color:var(--d)">Drill bounce: connecté à send_log.status = bounced, groupé par domaine et erreur SMTP</div>';}
}
function drillISP(isp){var c=document.getElementById('drill-content');c.innerHTML='<h4 style="font-size:11px;color:var(--am);margin-bottom:8px">Level 2: '+isp+' → Par heure (dernières 24h)</h4><div style="font-size:10px;color:var(--d)">Query: SELECT date_trunc(\'hour\', created_at), COUNT(*), inbox_rate FROM send_log WHERE isp=\''+isp+'\' GROUP BY 1</div><div style="margin-top:8px"><span class="drill-link" onclick="drillISPAccount(\''+isp+'\')">→ Level 3: Par compte d\'envoi</span></div>'}
function drillISPAccount(isp){var c=document.getElementById('drill-content');c.innerHTML='<h4 style="font-size:11px;color:var(--pu);margin-bottom:8px">Level 3: '+isp+' → Par compte d\'envoi</h4><div style="font-size:10px;color:var(--d)">Query: SELECT from_email, COUNT(*), inbox_rate FROM send_log WHERE isp=\''+isp+'\' GROUP BY 1 ORDER BY 2 DESC LIMIT 20</div>'}
function closeDrill(){document.getElementById('drill-panel').style.display='none'}
loadAll();setInterval(loadAll,30000);
setInterval(function(){document.getElementById('clock').textContent=new Date().toLocaleString('fr-FR')},1000);
</script><script src="arsenal-common.js"></script>
</script>