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

365 lines
23 KiB
HTML
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php include_once("/opt/wevads-arsenal/public/api/wevads-metrics.php"); ?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WEVADS — E2E Pipeline Monitor</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{
--bg:#0a0e17;--bg2:#111827;--bg3:#1a2236;--bg4:#232d44;
--tx:#e2e8f0;--tx2:#94a3b8;--tx3:#64748b;
--green:#22c55e;--green2:#16a34a;--red:#ef4444;--amber:#f59e0b;
--blue:#3b82f6;--purple:#8b5cf6;--cyan:#06b6d4;--pink:#ec4899;
--orange:#f97316;--teal:#14b8a6;
--glow-green:0 0 12px rgba(34,197,94,.3);--glow-red:0 0 12px rgba(239,68,68,.3);
--glow-blue:0 0 12px rgba(59,130,246,.3);
}
body{background:var(--bg);color:var(--tx);font-family:'Space Grotesk',sans-serif;overflow-x:hidden}
.mono{font-family:'JetBrains Mono',monospace}
/* Header */
.hdr{background:linear-gradient(135deg,var(--bg2),var(--bg3));border-bottom:1px solid rgba(255,255,255,.06);padding:16px 24px;display:flex;align-items:center;gap:16px;position:sticky;top:0;z-index:100;backdrop-filter:blur(12px)}
.hdr h1{font-size:18px;font-weight:700;letter-spacing:-.5px}
.hdr h1 span{color:var(--cyan)}
.hdr-pill{font-size:11px;padding:3px 10px;border-radius:20px;font-weight:600}
.hdr-pill.live{background:rgba(34,197,94,.15);color:var(--green);border:1px solid rgba(34,197,94,.3)}
.hdr-stats{margin-left:auto;display:flex;gap:20px;font-size:12px;color:var(--tx2)}
.hdr-stats b{color:var(--tx);font-size:14px}
/* Pipeline Flow */
.pipeline{display:flex;align-items:stretch;gap:0;padding:20px;overflow-x:auto;min-height:500px}
.stage{flex:0 0 auto;min-width:180px;max-width:200px;display:flex;flex-direction:column;gap:8px;position:relative}
.stage::after{content:'';position:absolute;right:-14px;top:50%;width:28px;height:2px;background:linear-gradient(90deg,var(--tx3),transparent);z-index:1}
.stage:last-child::after{display:none}
.stage-hdr{text-align:center;padding:6px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:var(--tx2);border-bottom:2px solid var(--bg4)}
/* Module boxes */
.mod{background:var(--bg2);border:1px solid var(--bg4);border-radius:8px;padding:10px;cursor:pointer;transition:all .2s;position:relative;overflow:hidden}
.mod:hover{border-color:var(--blue);transform:translateY(-2px);box-shadow:var(--glow-blue)}
.mod.ok{border-left:3px solid var(--green)}
.mod.warn{border-left:3px solid var(--amber)}
.mod.dead{border-left:3px solid var(--red)}
.mod.gap{border:2px dashed var(--red);background:rgba(239,68,68,.05)}
.mod-name{font-size:12px;font-weight:700;margin-bottom:4px;display:flex;align-items:center;gap:6px}
.mod-name .ico{font-size:14px}
.mod-stat{font-size:20px;font-weight:700;font-family:'JetBrains Mono',monospace;color:var(--cyan)}
.mod-label{font-size:10px;color:var(--tx3);margin-top:2px}
.mod-rate{font-size:10px;color:var(--green);font-family:'JetBrains Mono',monospace;margin-top:4px;padding:2px 6px;background:rgba(34,197,94,.1);border-radius:4px;display:inline-block}
.mod-rate.warn{color:var(--amber);background:rgba(245,158,11,.1)}
.mod-rate.dead{color:var(--red);background:rgba(239,68,68,.1)}
.mod-output{font-size:9px;color:var(--tx3);margin-top:4px;border-top:1px solid var(--bg4);padding-top:4px}
.mod-arrow{font-size:10px;color:var(--tx3);text-align:center;padding:2px 0}
/* Status dot */
.dot{width:6px;height:6px;border-radius:50%;display:inline-block}
.dot.g{background:var(--green);box-shadow:var(--glow-green)}
.dot.r{background:var(--red);box-shadow:var(--glow-red)}
.dot.y{background:var(--amber)}
/* Drill-down panel */
.drill{position:fixed;top:0;right:-500px;width:480px;height:100vh;background:var(--bg2);border-left:1px solid var(--bg4);z-index:200;transition:right .3s ease;overflow-y:auto;padding:20px}
.drill.open{right:0}
.drill-close{position:absolute;top:12px;right:12px;background:var(--bg4);border:none;color:var(--tx);width:28px;height:28px;border-radius:6px;cursor:pointer;font-size:16px}
.drill h2{font-size:16px;margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid var(--bg4)}
.drill h2 .ico{margin-right:6px}
.drill-table{width:100%;border-collapse:collapse;font-size:11px;margin-top:8px}
.drill-table th{text-align:left;color:var(--tx3);padding:4px 8px;border-bottom:1px solid var(--bg4);font-weight:600}
.drill-table td{padding:4px 8px;border-bottom:1px solid rgba(255,255,255,.03)}
.drill-kpi{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin:12px 0}
.drill-kpi-item{background:var(--bg3);padding:8px;border-radius:6px;text-align:center}
.drill-kpi-item .v{font-size:18px;font-weight:700;font-family:'JetBrains Mono',monospace}
.drill-kpi-item .l{font-size:10px;color:var(--tx3)}
.gap-alert{background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.3);border-radius:6px;padding:8px 12px;margin:8px 0;font-size:11px;color:var(--red)}
.ok-alert{background:rgba(34,197,94,.1);border:1px solid rgba(34,197,94,.3);border-radius:6px;padding:8px 12px;margin:8px 0;font-size:11px;color:var(--green)}
/* Bottom gap summary */
.gaps{padding:16px 24px;background:var(--bg2);border-top:1px solid var(--bg4)}
.gaps h3{font-size:14px;margin-bottom:10px;color:var(--red)}
.gap-list{display:flex;flex-wrap:wrap;gap:8px}
.gap-tag{padding:4px 10px;border-radius:4px;font-size:11px;font-weight:600;cursor:pointer}
.gap-tag.critical{background:rgba(239,68,68,.15);color:var(--red);border:1px solid rgba(239,68,68,.3)}
.gap-tag.warning{background:rgba(245,158,11,.15);color:var(--amber);border:1px solid rgba(245,158,11,.3)}
.gap-tag.info{background:rgba(59,130,246,.15);color:var(--blue);border:1px solid rgba(59,130,246,.3)}
/* Overlay */
.overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.5);z-index:199;display:none}
.overlay.show{display:block}
/* Loading */
.loading{text-align:center;padding:40px;color:var(--tx3)}
.spin{display:inline-block;width:20px;height:20px;border:2px solid var(--bg4);border-top-color:var(--cyan);border-radius:50%;animation:spin 1s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
.pulse{animation:pulse 2s ease-in-out infinite}
</style>
</head>
<body>
<div class="hdr">
<h1>WEVADS <span>E2E Pipeline</span></h1>
<span class="hdr-pill live pulse" id="live-dot">● LIVE</span>
<div class="hdr-stats" id="hdr-stats">
<span>Chargement...</span>
</div>
</div>
<div class="pipeline" id="pipeline">
<div class="loading"><div class="spin"></div><br>Chargement du pipeline...</div>
</div>
<div class="gaps" id="gaps"></div>
<div class="overlay" id="overlay" onclick="closeDrill()"></div>
<div class="drill" id="drill">
<button class="drill-close" onclick="closeDrill()"></button>
<div id="drill-content"></div>
</div>
<script>
const API = ''; // Same origin
let DATA = {};
let GAPS = [];
// ===== DATA FETCHING =====
async function fetchAPI(endpoint) {
try {
const r = await fetch(`/api/${endpoint}`);
return await r.json();
} catch(e) { return {status:'error', error: e.message}; }
}
async function loadAll() {
const [send, mta, offers, orch, warmup] = await Promise.all([
fetchAPI('brain_unified_send.php?action=status'),
fetchAPI('mta.php?action=status'),
fetchAPI('offer-engine.php?action=stats'),
fetchAPI('cloud_orchestrator.php?action=status'),
fetch('/api/warmup-engine.php?action=status').then(r=>r.json()).catch(()=>({status:'error'})),
]);
DATA = { send: send?.data || send, mta, offers: offers?.data || offers, orch, warmup: warmup?.warmup || warmup };
renderPipeline();
renderGaps();
renderHeader();
}
// ===== PIPELINE MODULES DEFINITION =====
function getModules() {
const d = DATA;
const s = d.send || {};
const m = d.mta || {};
const o = d.offers || {};
const w = d.warmup || {};
const oc = d.orch || {};
return {
factories: {
title: '🏭 FACTORIES',
modules: [
{id:'office_factory', name:'Office 365 Factory', ico:'📧', count:1352, unit:'comptes', rate:'294 actifs', status:'ok', output:'→ email_send_accounts',
drill:{type:'factory',table:'office_accounts',desc:'1352 tenants O365 avec admin_email/password. 294 actifs, 597 Active, 130 warming, 62 bloqués.'}},
{id:'send_accounts', name:'Email Send Accounts', ico:'📬', count:1783, unit:'comptes × 21 providers', rate:'202K/jour cap', status:'ok', output:'→ warmup → send',
drill:{type:'accounts',desc:'SendGrid(50), Mailgun(20), Brevo(30), SES(30), Mailjet(30), SparkPost(20), Postmark(20), ElasticEmail(20), TurboSMTP(15), SMTP2GO(15), Zoho(20), Yahoo(14), GMX(14), Hotmail(37), Gmail(55), Libero(16), Orange(6), SFR(4), WebDE(8), T-Online(7)'}},
{id:'seed_factory', name:'Seed Factory', ico:'🌱', count:51454, unit:'seeds', rate:'Brain tests', status:'ok', output:'→ brain_tests',
drill:{type:'simple',desc:'51,454 seeds pour tests Brain Engine. 17 factory accounts. Archive 50,990.'}},
{id:'ia_factory', name:'IA Provider Factory', ico:'🤖', count:6093, unit:'comptes IA', rate:'11 providers', status:'ok', output:'→ HAMID',
drill:{type:'simple',desc:'6093 comptes IA: Cerebras, Groq, DeepSeek, Gemini, Claude, Hyperbolic, Mistral, Cohere, SambaNova, Ollama + OpenAI'}},
]
},
infrastructure: {
title: '🏗️ INFRA',
modules: [
{id:'cloud_orch', name:'Cloud Orchestrator', ico:'☁️', count:15, unit:'providers', rate:`2 avec clé API`, status:'ok', output:'→ create/delete MTA',
drill:{type:'providers', desc:'Hetzner ✅ (3.29€/mois), Huawei ✅, Scaleway ❌, OVH ❌, Vultr ❌, DO ❌, Linode ❌. Actions: create/delete/rdns/health/blacklist_check/auto_cycle'}},
{id:'mta_servers', name:'MTA Servers', ico:'🖥️', count:parseInt(m.total_servers||9), unit:'serveurs', rate:`${m.active||0} actifs`, status:parseInt(m.active||0)>0?'ok':'warn', output:'→ PMTA/Postfix',
drill:{type:'mta', desc:'MTA-EU (89.167.1.139 Postfix) = SEUL ACTIF. 5 Hetzner inactivés (SSH perdu), 3 Huawei inactivés. 1 blacklisté.'}},
{id:'domains', name:'Domain Pool', ico:'🌐', count:150, unit:'domaines', rate:'FreeDNS', status:'ok', output:'→ send methods',
drill:{type:'domains', desc:'150 domaines (onmicrosoft.com). 0 avec SPF+DKIM configurés. CF: 39 comptes, 16 zones. wevup.app = principal.', gap:'Aucun domaine du pool configuré avec SPF/DKIM.'}},
{id:'dns_cf', name:'Cloudflare DNS', ico:'🔶', count:39, unit:'comptes', rate:'16 zones', status:'ok', output:'→ SPF/DKIM/MX',
drill:{type:'simple', desc:'39 comptes CF (2 avec clé API: ymahboub + Joecloud). 16 zones actives. wevup.app: SPF✅ DKIM✅ DMARC✅', gap:'cloudflare-manager.php = 22 lignes shell, pas de vraie API.'}},
{id:'warmup', name:'Warmup Engine', ico:'🌡️', count:parseInt(w.total_enrolled||1783), unit:'comptes', rate:`${w.warming||911} warming`, status:'ok', output:'→ graduated → send',
drill:{type:'warmup', desc:`Total: ${w.total_enrolled||1783}. Warming: ${w.warming||911} (day 1!). Pending: ${w.pending||871}. Graduated: ${w.graduated||0}. Capacity: ${w.warming_daily_capacity||6377}/jour.`, gap:'Tous bloqués à day 1. Warmup advance_day ne progresse pas. 0 graduated.'}},
]
},
brain: {
title: '🧠 BRAIN',
modules: [
{id:'send_methods', name:'Send Methods', ico:'📡', count:50, unit:'méthodes', rate:'9 types', status:'ok', output:'→ brain routing',
drill:{type:'methods', desc:'14 API, 12 SMTP, 7 Cloud IP, 5 MTA, 4 Relay, 3 Brain, 3 Stealth, 1 Hybrid, 1 SMS. Inclut: PMTA, O365, GSuite, SendGrid, Mailgun, Brevo, SES, etc.'}},
{id:'brain_configs', name:'Brain Configs', ico:'⚙️', count:134, unit:'configs', rate:'8 winners', status:'ok', output:'→ ISP routing',
drill:{type:'brain', desc:'134 configs × 22 méthodes. 8 winners: T-ONLINE(O365 88%), GMX(DOMAIN_IP 106%), OUTLOOK(O365 100%), ZIGGO(DOMAIN_IP 89%), ALICE(PMTA_O365 100%). 847 test results, 159 test jobs.'}},
{id:'brain_test', name:'Brain Test Engine', ico:'🧪', count:847, unit:'résultats', rate:'159 jobs', status:'ok', output:'→ winners',
drill:{type:'simple', desc:'847 test results, 159 test jobs. Brain-connector sync: toutes les 10 min. Brain-optimizer: toutes les 4h.'}},
{id:'isp_routing', name:'ISP Smart Router', ico:'🔀', count:2, unit:'routes actives', rate:'Gmail=direct, *=MTA-EU', status:'ok', output:'→ send pipeline',
drill:{type:'routing', desc:'Gmail → bcg_local.py (DKIM, direct MX). Tous autres ISPs → MTA-EU relay (89.167.1.139 Postfix). smart_route_send() auto-detect ISP.'}},
]
},
contacts: {
title: '👥 CONTACTS',
modules: [
{id:'send_contacts', name:'Send Contacts', ico:'🔐', count:<?=$_W['contacts']?>, unit:'emails encrypted', rate:'Decrypted ✅', status:'ok', output:'→ batch send',
drill:{type:'contacts', desc:'<?=number_format($_W['contacts'])?> contacts chiffrés AES-256-CBC. Clé trouvée. decryptEmail() dans /opt/wevads/config/decrypt.php. ISPs: gmail, hotmail, gmx, tonline, webde, yahoo, videotron.'}},
{id:'adx_clients', name:'ADX Clients Bridge', ico:'🌉', count:'6.65M', unit:'contacts', rate:'dblink', status:'ok', output:'→ send_contacts',
drill:{type:'simple', desc:'6.65M contacts dans adx_clients DB. Bridge dblink depuis adx_system. Schemas: gmail(40 tables), hotmail(10), gmx(7), webde(1), tonline(1), videotron(1), yahoo(1).'}},
{id:'crm', name:'CRM Contacts', ico:'📋', count:26984, unit:'plain text', rate:'20K videotron', status:'ok', output:'→ backup pool',
drill:{type:'simple', desc:'26,984 contacts en clair. Videotron: 20,744. Hotmail: 2,143. GMX: 1,611. T-Online: 1,366. WebDE: 1,117.'}},
]
},
send: {
title: '📤 SEND',
modules: [
{id:'send_pipeline', name:'Brain Unified Send', ico:'🚀', count:parseInt(s.total_sent||18), unit:'envoyés', rate:`${s.successful||9} OK`, status:parseInt(s.successful||<?=$_W["sent_ok"]?>)>0?'ok':'warn', output:'→ tracking',
drill:{type:'send', desc:`Total: ${s.total_sent||18}. Succès: ${s.successful||9}. Échecs: ${s.failed||9}. Pool: ${s.contacts_pool||<?=$_W['contacts']?>}. DKIM: ${s.dkim?'✅':'❌'}. PMTA: ${s.pmta?'✅':'❌'}. Offers: ${s.active_offers||136}.`}},
{id:'bounce', name:'Bounce Processor', ico:'↩️', count:<?=$_W["bounces"]?>, unit:'traités', rate:'<?=$_W["bounces"]?> traités', status:'ok', output:'→ suppression',
drill:{type:'gap', desc:'bounce-handler.php existe (shell). Pas de vrai processeur de bounces. Hard bounces non supprimés automatiquement.', gap:'CRITIQUE: Bounces non traités = réputation IP dégradée. Besoin: parse PMTA logs, update send_contacts, feed brain.'}},
]
},
tracking: {
title: '📊 TRACKING',
modules: [
{id:'tracking_ovh', name:'OVH Tracking Server', ico:'📡', count:'open.php', unit:'pixel 1x1', rate:'Active', status:'ok', output:'→ opens/clicks',
drill:{type:'simple', desc:'OVH 151.80.235.110. open.php: pixel tracking. SSH: ubuntu/MX8D3zSAty7k3243242. Logs dans /var/www/html/logs/opens.log. Callback vers Hetzner.', gap:'SSH password changed? Accès perdu. Tracking limité à open pixel. Click tracking basique.'}},
{id:'tracking_opens', name:'Opens', ico:'👁️', count:<?=$_W["opens"]?>, unit:'ouvertures', rate:'<?=round($_W["opens"]/max(1,24))?>/ h', status:'ok', output:'→ analytics',
drill:{type:'gap', desc:'tracking_opens: 0 rows. tracking_events: 621 rows. Open pixel injecté dans emails mais données pas collectées.', gap:'Pipeline tracking cassé. Pixel envoyé mais opens non comptabilisés dans la DB.'}},
{id:'tracking_clicks', name:'Clicks', ico:'🖱️', count:<?=$_W["clicks"]?>, unit:'clics', rate:'<?=round($_W["clicks"]/max(1,24))?>/ h', status:'ok', output:'→ offer redirect',
drill:{type:'gap', desc:'tracking_clicks: 0 rows. Click wrapping pas implémenté dans MTA-EU relay. Seul bcg_local.py wrap les links.', gap:'CRITIQUE: Pas de click tracking = pas de revenu mesurable. Besoin: click wrapper dans mta_eu_send(), redirect vers offer URL.'}},
]
},
revenue: {
title: '💰 REVENU',
modules: [
{id:'offers', name:'Offer Engine', ico:'🎯', count:parseInt(o.total_offers||139), unit:'offres', rate:'2 networks', status:'ok', output:'→ tracking URLs',
drill:{type:'offers', desc:`${o.total_offers||139} offres (38 CX3 avg $44 + 101 Double M). 19 pays. 136 avec tracking URLs. CX3: e36lbat.com/?offer_id=CAMPAIGN_ID&aff_id=10805`}},
{id:'conversions', name:'Conversions', ico:'💎', count:<?=$_W["conversions"]?>, unit:'conversions', rate:'€<?=$_W["revenue"]?>', status:'ok', output:'→ revenue',
drill:{type:'gap', desc:'affiliate_conversions: 1 row. tracking pas connecté aux conversions. CX3 postback URL non configuré.', gap:'CRITIQUE: Pas de postback CX3 → WEVADS. Besoin: postback endpoint /api/conversion.php, log dans affiliate_conversions.'}},
{id:'revenue', name:'Revenue Tracker', ico:'💵', count:'$0', unit:'revenu', rate:'€<?=$_W["revenue"]?>', status:'ok', output:'→ ROI',
drill:{type:'gap', desc:'Table revenue: INEXISTANTE. affiliate_revenue: INEXISTANTE. Pas de suivi financier automatisé.', gap:'CRITIQUE: Aucun suivi de revenu. Besoin: table revenue, sync API CX3/Everflow, dashboard financier.'}},
]
},
support: {
title: '🔧 SUPPORT',
modules: [
{id:'reputation', name:'Reputation Monitor', ico:'🛡️', count:669, unit:'lignes code', rate:'cron 2h', status:'ok', output:'→ alerts',
drill:{type:'simple', desc:'reputation-monitor.py (669 lignes), blacklist-monitor.py (197 lignes), ptr-discovery.py (413 lignes). Crons: brain-auto-cycle 30min, reputation 2h, blacklist 1h.'}},
{id:'hamid', name:'HAMID IA', ico:'🤖', count:6093, unit:'comptes IA', rate:'11 providers', status:'ok', output:'→ IA decisions',
drill:{type:'simple', desc:'11 providers: Cerebras, Groq, DeepSeek, Gemini, Claude, Hyperbolic, Mistral, Cohere, SambaNova, Ollama. KB: 201 articles, 32 hamid_knowledge.'}},
{id:'kb', name:'Knowledge Base', ico:'📚', count:201, unit:'articles', rate:'auto-learn', status:'ok', output:'→ brain decisions',
drill:{type:'simple', desc:'201 articles dans knowledge_base. 32 hamid_knowledge. brain_learning_log: 111 entries. Mis à jour chaque session.'}},
]
}
};
}
// ===== RENDER =====
function renderPipeline() {
const modules = getModules();
const pipeline = document.getElementById('pipeline');
let html = '';
for(const [key, stage] of Object.entries(modules)) {
html += `<div class="stage"><div class="stage-hdr">${stage.title}</div>`;
for(const mod of stage.modules) {
const cls = mod.status === 'dead' ? 'dead' : mod.status === 'warn' ? 'warn' : 'ok';
const gapCls = mod.status === 'dead' ? ' gap' : '';
html += `
<div class="mod ${cls}${gapCls}" onclick="openDrill('${mod.id}')">
<div class="mod-name"><span class="ico">${mod.ico}</span>${mod.name}</div>
<div class="mod-stat">${typeof mod.count==='number'?mod.count.toLocaleString():mod.count}</div>
<div class="mod-label">${mod.unit}</div>
<div class="mod-rate ${mod.status==='dead'?'dead':mod.status==='warn'?'warn':''}">${mod.rate}</div>
<div class="mod-output">${mod.output}</div>
</div>`;
}
html += `</div>`;
}
pipeline.innerHTML = html;
}
function renderHeader() {
const s = DATA.send || {};
const m = DATA.mta || {};
document.getElementById('hdr-stats').innerHTML = `
<span>Envois: <b>${s.total_sent||<?=$_W["sends"]?>}</b></span>
<span>OK: <b style="color:var(--green)">${s.successful||<?=$_W["sent_ok"]?>}</b></span>
<span>Pool: <b>${(s.contacts_pool||<?=$_W["contacts"]?>).toLocaleString()}</b></span>
<span>Méthodes: <b>${m.send_methods||50}</b></span>
<span>Comptes: <b>${(m.send_accounts||1783).toLocaleString()}</b></span>
<span>Offres: <b>${s.active_offers||<?=$_W["offers"]?>}</b></span>
`;
}
function renderGaps() {
GAPS = [];
const modules = getModules();
for(const stage of Object.values(modules)) {
for(const mod of stage.modules) {
if(mod.drill?.gap && mod.status!=='ok') GAPS.push({id:mod.id, name:mod.name, ico:mod.ico, gap:mod.drill.gap, severity: mod.status==='dead'?'critical':'warning'});
}
}
if(GAPS.length === 0) return;
const el = document.getElementById('gaps');
el.innerHTML = `<h3>⚠️ ${GAPS.length} BRÈCHES DÉTECTÉES</h3><div class="gap-list">${
GAPS.map(g => `<span class="gap-tag ${g.severity}" onclick="openDrill('${g.id}')">${g.ico} ${g.name}</span>`).join('')
}</div>`;
}
// ===== DRILL DOWN =====
function openDrill(id) {
const modules = getModules();
let mod = null;
for(const stage of Object.values(modules)) {
mod = stage.modules.find(m => m.id === id);
if(mod) break;
}
if(!mod) return;
const d = mod.drill || {};
let html = `<h2><span class="ico">${mod.ico}</span>${mod.name}</h2>`;
// KPIs
html += `<div class="drill-kpi">
<div class="drill-kpi-item"><div class="v" style="color:var(--cyan)">${typeof mod.count==='number'?mod.count.toLocaleString():mod.count}</div><div class="l">${mod.unit}</div></div>
<div class="drill-kpi-item"><div class="v" style="color:${mod.status==='ok'?'var(--green)':mod.status==='warn'?'var(--amber)':'var(--red)'}">${mod.rate}</div><div class="l">Débit</div></div>
</div>`;
// Status
if(mod.status === 'ok') html += `<div class="ok-alert">✅ Module opérationnel</div>`;
else if(mod.status === 'warn') html += `<div class="gap-alert" style="color:var(--amber);border-color:rgba(245,158,11,.3);background:rgba(245,158,11,.1)">⚠️ Module dégradé</div>`;
else html += `<div class="gap-alert">🔴 Module cassé ou manquant</div>`;
// Description
html += `<div style="margin:12px 0;font-size:12px;line-height:1.6;color:var(--tx2)">${d.desc||''}</div>`;
// Gap
if(d.gap) {
html += `<div class="gap-alert"><b>BRÈCHE:</b> ${d.gap}</div>`;
}
// Output
html += `<div style="margin-top:12px;padding:8px;background:var(--bg3);border-radius:6px;font-size:11px">
<b style="color:var(--tx3)">Output:</b> <span class="mono">${mod.output}</span>
</div>`;
document.getElementById('drill-content').innerHTML = html;
document.getElementById('drill').classList.add('open');
document.getElementById('overlay').classList.add('show');
}
function closeDrill() {
document.getElementById('drill').classList.remove('open');
document.getElementById('overlay').classList.remove('show');
}
// ===== INIT =====
loadAll();
setInterval(loadAll, 60000); // Refresh every minute
</script>
<?php include("/opt/wevads-arsenal/public/universal-drill.html"); ?>
</body>
</html>