365 lines
23 KiB
HTML
Executable File
365 lines
23 KiB
HTML
Executable File
<?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>
|