349 lines
69 KiB
Plaintext
349 lines
69 KiB
Plaintext
<!DOCTYPE html>
|
||
<html lang="fr"><head>
|
||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||
<title>WEVADS IA — Omnichannel Intelligence Platform</title>
|
||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root{--bg:#060a11;--bg2:#0b1120;--sf:#101828;--card:#131d30;--hov:#182540;--bd:rgba(255,255,255,.05);--bd2:rgba(255,255,255,.09);--tx:#a8b8cc;--mu:#5c6f85;--dm:#3a4a5c;--wh:#ecf0f5;--ac:#d4a843;--ac2:#e8c55a;--acB:rgba(212,168,67,.07);--acBd:rgba(212,168,67,.2);--cy:#22d3ee;--gn:#34d399;--gnB:rgba(52,211,153,.07);--rd:#f87171;--rdB:rgba(248,113,113,.07);--or:#fb923c;--pu:#a78bfa;--puB:rgba(167,139,250,.07);--bl:#60a5fa;--blB:rgba(96,165,250,.07);--em:#60a5fa;--sm:#34d399;--wa:#25d366;--tg:#229ED9;--r:12px;--r2:16px;--r3:8px;--f:'Outfit',system-ui,sans-serif;--m:'JetBrains Mono',monospace}
|
||
*{margin:0;padding:0;box-sizing:border-box}
|
||
body{font-family:var(--f);background:var(--bg);color:var(--tx);display:flex;height:100vh;overflow:hidden;font-size:14px;-webkit-font-smoothing:antialiased}
|
||
::-webkit-scrollbar{width:4px}::-webkit-scrollbar-thumb{background:var(--bd2);border-radius:2px}::-webkit-scrollbar-track{background:transparent}
|
||
a{color:var(--ac);text-decoration:none}button{font-family:var(--f);cursor:pointer;border:none;outline:none}
|
||
input,select,textarea{font-family:var(--f);outline:none;background:var(--bg);border:1px solid var(--bd2);color:var(--wh);border-radius:var(--r3);padding:10px 14px;font-size:13px;transition:.2s}
|
||
input:focus,select:focus,textarea:focus{border-color:var(--ac);box-shadow:0 0 0 3px rgba(212,168,67,.07)}
|
||
table{width:100%;border-collapse:collapse}th{text-align:left;font-size:10px;font-weight:700;color:var(--mu);text-transform:uppercase;letter-spacing:.08em;padding:10px 14px;border-bottom:1px solid var(--bd)}
|
||
td{padding:11px 14px;border-bottom:1px solid var(--bd);font-size:13px}tr:hover td{background:rgba(255,255,255,.015)}
|
||
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
|
||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
|
||
@keyframes glow{0%,100%{box-shadow:0 0 20px rgba(212,168,67,.1)}50%{box-shadow:0 0 40px rgba(212,168,67,.2)}}
|
||
@keyframes spin{to{transform:rotate(360deg)}}
|
||
.login-wrap{position:fixed;inset:0;background:var(--bg);display:flex;align-items:center;justify-content:center;z-index:200}
|
||
.login-wrap::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse 600px 400px at 50% 40%,rgba(212,168,67,.03),transparent);pointer-events:none}
|
||
.login-box{background:var(--card);border:1px solid var(--bd2);border-radius:24px;padding:44px;width:420px;max-width:92vw;animation:fadeIn .5s;position:relative;overflow:hidden}
|
||
.login-box::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,var(--ac),transparent)}
|
||
.login-logo{width:50px;height:50px;background:linear-gradient(135deg,var(--ac),var(--ac2));border-radius:14px;display:flex;align-items:center;justify-content:center;margin:0 auto 18px;font-weight:900;color:#0a0d13;font-size:20px;animation:glow 3s infinite}
|
||
.login-box h1{font-size:22px;font-weight:800;color:var(--wh);text-align:center;letter-spacing:-.03em}
|
||
.login-box .sub{font-size:12px;color:var(--mu);text-align:center;margin:4px 0 4px}.login-box .ver{font-size:10px;color:var(--dm);text-align:center;margin-bottom:20px;font-family:var(--m)}
|
||
.field{margin-bottom:12px}.field label{display:block;font-size:10px;font-weight:600;color:var(--mu);margin-bottom:4px;text-transform:uppercase;letter-spacing:.06em}.field input{width:100%}
|
||
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:10px 20px;border-radius:10px;font-size:13px;font-weight:600;transition:.2s}
|
||
.btn-full{width:100%}.btn-ac{background:linear-gradient(135deg,var(--ac),var(--ac2));color:#0a0d13}.btn-ac:hover{filter:brightness(1.1);transform:translateY(-1px)}
|
||
.btn-gh{background:transparent;color:var(--tx);border:1px solid var(--bd2)}.btn-gh:hover{border-color:var(--mu);background:rgba(255,255,255,.02)}
|
||
.btn-sm{padding:7px 14px;font-size:12px;border-radius:var(--r3)}.btn-rd{background:var(--rdB);color:var(--rd);border:1px solid rgba(248,113,113,.12)}
|
||
.divider{display:flex;align-items:center;gap:12px;margin:18px 0;color:var(--dm);font-size:11px}.divider::before,.divider::after{content:'';flex:1;height:1px;background:var(--bd)}
|
||
.sidebar{width:250px;background:var(--bg2);border-right:1px solid var(--bd);display:flex;flex-direction:column;flex-shrink:0}
|
||
.sb-brand{padding:18px;display:flex;align-items:center;gap:11px;border-bottom:1px solid var(--bd)}
|
||
.sb-logo{width:34px;height:34px;background:linear-gradient(135deg,var(--ac),var(--ac2));border-radius:10px;display:flex;align-items:center;justify-content:center;font-weight:900;color:#0a0d13;font-size:15px}
|
||
.sb-brand-name{font-size:15px;font-weight:800;color:var(--wh);letter-spacing:-.02em}.sb-brand-sub{font-size:8px;color:var(--dm);font-family:var(--m);letter-spacing:.05em}
|
||
.sb-ch{display:flex;gap:5px;padding:12px 16px;border-bottom:1px solid var(--bd)}
|
||
.sb-chip{flex:1;height:26px;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:700;position:relative}
|
||
.sb-chip.on{opacity:1}.sb-chip.off{opacity:.3}
|
||
.sb-chip-em{background:rgba(96,165,250,.08);color:var(--em);border:1px solid rgba(96,165,250,.12)}.sb-chip-sm{background:rgba(52,211,153,.08);color:var(--sm);border:1px solid rgba(52,211,153,.12)}
|
||
.sb-chip-wa{background:rgba(37,211,102,.08);color:var(--wa);border:1px solid rgba(37,211,102,.12)}.sb-chip-tg{background:rgba(34,158,217,.08);color:var(--tg);border:1px solid rgba(34,158,217,.12)}
|
||
.sb-chip .dot{position:absolute;top:2px;right:2px;width:5px;height:5px;border-radius:50%}.dot-on{background:var(--gn)}.dot-off{background:var(--rd)}
|
||
.sb-nav{flex:1;overflow-y:auto;padding:6px 8px}
|
||
.sb-sec{padding:12px 10px 5px;font-size:8px;font-weight:700;color:var(--dm);text-transform:uppercase;letter-spacing:.12em}
|
||
.sb-it{display:flex;align-items:center;gap:9px;padding:9px 12px;border-radius:9px;cursor:pointer;font-size:12.5px;color:var(--mu);font-weight:500;transition:.15s;margin-bottom:1px}
|
||
.sb-it:hover{background:rgba(255,255,255,.025);color:var(--tx)}.sb-it.on{background:var(--acB);color:var(--ac);border:1px solid var(--acBd)}
|
||
.sb-it svg{width:16px;height:16px;flex-shrink:0;opacity:.5}.sb-it.on svg{opacity:1}
|
||
.sb-it .cnt{margin-left:auto;font-size:9px;font-family:var(--m);background:var(--acB);color:var(--ac);padding:2px 6px;border-radius:8px;font-weight:600}
|
||
.sb-user{padding:12px 14px;border-top:1px solid var(--bd);display:flex;align-items:center;gap:9px}
|
||
.sb-av{width:30px;height:30px;border-radius:9px;background:linear-gradient(135deg,var(--ac),var(--ac2));display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:800;color:#0a0d13;flex-shrink:0}
|
||
.sb-un{font-size:11px;font-weight:600;color:var(--wh)}.sb-ut{font-size:9px;color:var(--dm);font-family:var(--m)}
|
||
.sb-out{padding:5px;border-radius:7px;background:transparent;color:var(--dm);cursor:pointer;transition:.15s;margin-left:auto}.sb-out:hover{background:var(--rdB);color:var(--rd)}
|
||
.main{flex:1;display:flex;flex-direction:column;overflow:hidden}
|
||
.top{height:50px;border-bottom:1px solid var(--bd);display:flex;align-items:center;padding:0 24px;gap:12px;background:var(--bg2);flex-shrink:0}
|
||
.top h2{font-size:16px;font-weight:700;color:var(--wh);flex:1;letter-spacing:-.02em}
|
||
.top-live{font-family:var(--m);font-size:9px;padding:4px 10px;border-radius:16px;display:flex;align-items:center;gap:5px;color:var(--gn);background:var(--gnB);border:1px solid rgba(52,211,153,.12)}
|
||
.top-live::before{content:'';width:5px;height:5px;border-radius:50%;background:var(--gn);animation:pulse 2s infinite}
|
||
.content{flex:1;overflow-y:auto;padding:24px;animation:fadeIn .25s}
|
||
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(165px,1fr));gap:12px;margin-bottom:24px}
|
||
.stat{background:var(--card);border:1px solid var(--bd);border-radius:var(--r2);padding:18px;position:relative;overflow:hidden;transition:.2s}.stat:hover{border-color:var(--bd2)}
|
||
.stat::before{content:'';position:absolute;top:0;left:0;right:0;height:2px}
|
||
.s-ac::before{background:linear-gradient(90deg,var(--ac),var(--ac2))}.s-bl::before{background:var(--bl)}.s-gn::before{background:var(--gn)}.s-rd::before{background:var(--rd)}.s-pu::before{background:var(--pu)}.s-tg::before{background:var(--tg)}.s-or::before{background:var(--or)}
|
||
.st-l{font-size:10px;font-weight:600;color:var(--mu);text-transform:uppercase;letter-spacing:.05em;margin-bottom:5px}
|
||
.st-v{font-size:26px;font-weight:800;color:var(--wh);font-family:var(--m);letter-spacing:-.04em;line-height:1}
|
||
.st-s{font-size:10px;margin-top:5px;font-weight:500;color:var(--mu)}.st-s .up{color:var(--gn)}.st-s .dn{color:var(--rd)}
|
||
.card{background:var(--card);border:1px solid var(--bd);border-radius:var(--r2);padding:20px;margin-bottom:16px}
|
||
.card-h{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}
|
||
.card-t{font-size:14px;font-weight:700;color:var(--wh);letter-spacing:-.01em}
|
||
.badge{display:inline-flex;padding:3px 9px;border-radius:16px;font-size:9px;font-weight:600;letter-spacing:.02em}
|
||
.b-ok{background:var(--gnB);color:var(--gn)}.b-sent{background:var(--gnB);color:var(--gn)}.b-wait{background:rgba(251,146,60,.07);color:var(--or)}.b-err{background:var(--rdB);color:var(--rd)}.b-em{background:var(--blB);color:var(--bl)}.b-ac{background:var(--acB);color:var(--ac)}
|
||
.modal-bg{position:fixed;inset:0;background:rgba(0,0,0,.6);backdrop-filter:blur(3px);z-index:100;display:flex;align-items:center;justify-content:center;animation:fadeIn .12s}
|
||
.modal{background:var(--card);border:1px solid var(--bd2);border-radius:var(--r2);padding:28px;width:560px;max-width:92vw;max-height:85vh;overflow-y:auto}
|
||
.modal h3{font-size:17px;font-weight:700;color:var(--wh);margin-bottom:18px}.modal .brow{display:flex;gap:8px;margin-top:18px}
|
||
.ch-sel{display:flex;gap:6px;margin:10px 0}.ch-p{padding:7px 14px;border-radius:16px;font-size:11px;font-weight:600;cursor:pointer;transition:.2s;border:1px solid var(--bd2);background:transparent;color:var(--mu)}
|
||
.ch-p.sel{border-color:currentColor}.ch-p[data-c=email].sel{background:var(--blB);color:var(--em)}.ch-p[data-c=sms].sel{background:var(--gnB);color:var(--sm)}.ch-p[data-c=whatsapp].sel{background:rgba(37,211,102,.07);color:var(--wa)}.ch-p[data-c=telegram].sel{background:rgba(34,158,217,.07);color:var(--tg)}
|
||
.chat-w{display:flex;flex-direction:column;height:100%}.chat-m{flex:1;overflow-y:auto;padding:0 4px 14px}
|
||
.chat-e{text-align:center;padding:50px 20px;color:var(--dm)}.chat-e h3{color:var(--mu);margin-top:10px;font-size:15px}
|
||
.chat-u{background:var(--acB);border:1px solid var(--acBd);border-radius:var(--r) var(--r) 4px var(--r);padding:12px 16px;margin:7px 0;max-width:75%;margin-left:auto;font-size:13px;color:var(--wh);line-height:1.6}
|
||
.chat-a{background:var(--sf);border:1px solid var(--bd);border-radius:4px var(--r) var(--r) var(--r);padding:14px 18px;margin:7px 0;max-width:85%;font-size:13px;line-height:1.7}
|
||
.chat-a pre{background:var(--bg);padding:10px;border-radius:var(--r3);overflow-x:auto;font-family:var(--m);font-size:12px;margin:7px 0}.chat-a code{font-family:var(--m);font-size:12px;background:var(--bg);padding:2px 5px;border-radius:3px}
|
||
.chat-b{display:flex;gap:8px;padding:12px 0 0;border-top:1px solid var(--bd)}.chat-b textarea{flex:1;resize:none;min-height:42px;max-height:110px;font-size:13px;line-height:1.5;border-radius:10px}
|
||
.ldot{display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--ac);margin:0 2px;animation:pulse .8s infinite}.ldot:nth-child(2){animation-delay:.15s}.ldot:nth-child(3){animation-delay:.3s}
|
||
.spinner{width:16px;height:16px;border:2px solid var(--bd);border-top-color:var(--ac);border-radius:50%;animation:spin .6s linear infinite;display:inline-block}
|
||
.toolbar{display:flex;align-items:center;gap:8px;margin-bottom:16px;flex-wrap:wrap}.toolbar .sp{flex:1}
|
||
.toolbar input[type=search]{width:260px;background:var(--sf);padding-left:34px;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='13' height='13' viewBox='0 0 24 24' fill='none' stroke='%235c6f85' stroke-width='2'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:11px center}
|
||
.toast{position:fixed;bottom:22px;right:22px;background:var(--card);color:var(--wh);padding:12px 20px;border-radius:var(--r);font-size:13px;z-index:9999;display:none;border:1px solid var(--bd2);box-shadow:0 10px 35px rgba(0,0,0,.5);animation:fadeIn .15s}
|
||
.toast.err{border-color:rgba(248,113,113,.15);color:var(--rd)}
|
||
.tags{display:flex;flex-wrap:wrap;gap:4px}.tag{padding:2px 8px;border-radius:12px;font-size:9px;font-weight:600;background:var(--sf);border:1px solid var(--bd);color:var(--mu)}
|
||
.ed{width:100%;min-height:180px;font-family:var(--m);font-size:12px;line-height:1.6;background:var(--bg);border-radius:var(--r3);padding:14px;resize:vertical}
|
||
.row2{display:grid;grid-template-columns:1fr 1fr;gap:16px}
|
||
@media(max-width:768px){.sidebar{width:55px}.sb-brand-name,.sb-brand-sub,.sb-sec,.sb-it span,.sb-un,.sb-ut,.sb-ch{display:none}.sb-it{justify-content:center;padding:9px}.content{padding:14px}.row2{grid-template-columns:1fr}}
|
||
</style></head><body>
|
||
<div class="login-wrap" id="LW"><div class="login-box"><div class="login-logo" style="padding:6px;background:none;animation:none"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" style="width:100%;height:100%"><defs><linearGradient id="wg" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#d4a843"/><stop offset="100%" stop-color="#e8c55a"/></linearGradient></defs><rect width="48" height="48" rx="12" fill="url(#wg)"/><path d="M12 16l12 9 12-9" stroke="#0a0d13" stroke-width="2.2" fill="none" stroke-linecap="round"/><rect x="12" y="15" width="24" height="18" rx="3" stroke="#0a0d13" stroke-width="1.5" fill="none" opacity=".5"/><text x="36" y="14" text-anchor="middle" fill="#0a0d13" font-size="10" font-weight="800" font-family="system-ui">IA</text></svg></div><h1>WEVADS IA</h1><p class="sub">Omnichannel Intelligence Platform</p><p class="ver">v3.0 — Sovereign Engine · 344 APIs · 688 Tables</p>
|
||
<div id="LF"><div class="field"><label>Email</label><input type="email" id="lE" placeholder="you@company.com"></div><div class="field"><label>Mot de passe</label><input type="password" id="lP" onkeydown="if(event.key==='Enter')doLogin()"></div><button class="btn btn-ac btn-full" onclick="doLogin()" id="bL">Se connecter</button><div class="divider">ou</div><button class="btn btn-gh btn-full" onclick="sR()">Créer un compte</button></div>
|
||
<div id="RF" style="display:none"><div class="field"><label>Nom</label><input id="rN"></div><div class="field"><label>Email</label><input type="email" id="rE"></div><div class="field"><label>Entreprise</label><input id="rC"></div><div class="field"><label>Mot de passe</label><input type="password" id="rP" onkeydown="if(event.key==='Enter')doReg()"></div><button class="btn btn-ac btn-full" onclick="doReg()" id="bR">Créer</button><div class="divider">ou</div><button class="btn btn-gh btn-full" onclick="sL()">Connexion</button></div>
|
||
</div></div>
|
||
<div class="sidebar" id="SB" style="display:none"><div class="sb-brand"><div class="sb-logo" style="padding:4px;background:none"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" style="width:100%;height:100%"><defs><linearGradient id="wg" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#d4a843"/><stop offset="100%" stop-color="#e8c55a"/></linearGradient></defs><rect width="48" height="48" rx="12" fill="url(#wg)"/><path d="M12 16l12 9 12-9" stroke="#0a0d13" stroke-width="2.2" fill="none" stroke-linecap="round"/><rect x="12" y="15" width="24" height="18" rx="3" stroke="#0a0d13" stroke-width="1.5" fill="none" opacity=".5"/><text x="36" y="14" text-anchor="middle" fill="#0a0d13" font-size="10" font-weight="800" font-family="system-ui">IA</text></svg></div><div><span class="sb-brand-name">WEVADS IA</span><br><span class="sb-brand-sub">v3.0 OMNICHANNEL</span></div></div>
|
||
<div class="sb-ch"><div class="sb-chip sb-chip-em on"><span class="dot dot-on"></span>✉</div><div class="sb-chip sb-chip-sm off"><span class="dot dot-off"></span>SMS</div><div class="sb-chip sb-chip-wa off"><span class="dot dot-off"></span>WA</div><div class="sb-chip sb-chip-tg on"><span class="dot dot-on"></span>TG</div></div>
|
||
<div class="sb-nav" id="SN"></div>
|
||
<div class="sb-user"><div class="sb-av" id="sA">W</div><div style="flex:1;min-width:0"><div class="sb-un" id="sU"></div><div class="sb-ut" id="sT">admin</div></div><div class="sb-out" onclick="logout()" title="Déconnexion"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg></div></div></div>
|
||
<div class="main" id="MA" style="display:none"><div class="top"><h2 id="PT">Dashboard</h2><div class="top-live" id="TL">Backend Live</div></div><div class="content" id="C"></div></div>
|
||
<div class="toast" id="T"></div>
|
||
<script>
|
||
const API='/api/v2',V2E='/api/wevads-v2-engine.php',BRG='/api/adx-bridge.php',WEVIA='/api/weval-ia-full',SE='/api/send-engine-unified.php';
|
||
let TK='',U={},PG='dashboard';
|
||
const $=id=>document.getElementById(id),v=id=>$(id)?$(id).value.trim():'';
|
||
const P=[
|
||
['dashboard','Dashboard','M4 3h16a1 1 0 0 1 1 1v7H3V4a1 1 0 0 1 1-1zM3 14h18v7a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-7z','Principal'],
|
||
['send','⚡ Envoyer','M13 2L3 14h9l-1 8 10-12h-9l1-8z','Principal'],
|
||
['campaigns','Campagnes','M2 4h20v16H2V4zm0 4h20M2 8l10 5 10-5','Principal'],
|
||
['contacts','Contacts','M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2M9 7a4 4 0 1 0 0-8 4 4 0 0 0 0 8M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75','Principal'],
|
||
['senders','Senders','M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2zM22 6l-10 7L2 6','Email Ops'],
|
||
['warmup','Warmup','M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83','Email Ops'],
|
||
['domains','Domaines','M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zM2 12h20M12 2a15 15 0 0 1 4 10 15 15 0 0 1-4 10 15 15 0 0 1-4-10 15 15 0 0 1 4-10z','Email Ops'],
|
||
['seeds','Seeds','M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z','Email Ops'],
|
||
['reputation','Réputation','M9 12l2 2 4-4M20.6 6.7A9.94 9.94 0 0 0 12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10c0-1.5-.3-2.9-.9-4.2','Email Ops'],
|
||
['brain','Brain IA','M4 4h16v16H4V4zM9 9h6v6H9V9zM9 1v3M15 1v3M9 20v3M15 20v3M20 9h3M20 14h3M1 9h3M1 14h3','Intelligence'],
|
||
['creative','Creative','M12 20h9M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z','Intelligence'],
|
||
['analytics','Analytics','M18 20V10M12 20V4M6 20v-6','Intelligence'],
|
||
['ai','AI Assistant','M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z','Intelligence'],
|
||
['channels','Canaux','M12 12m-2 0a2 2 0 1 0 4 0 2 2 0 1 0-4 0M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14','Omnichannel'],
|
||
['templates','Templates','M3 3h18v18H3V3zM3 9h18M9 21V9','Omnichannel'],
|
||
['offers','Offres','M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82zM7 7h.01','Business'],
|
||
['sponsors','Sponsors','M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2M9 7a4 4 0 1 0 0-8 4 4 0 0 0 0 8M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75','Business'],
|
||
['servers','Serveurs','M22 12h-6l-2 3h-4l-2-3H2M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z','Système'],
|
||
['infra','Infrastructure','M22 12h-6l-2 3h-4l-2-3H2M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z','Système'],
|
||
['settings','Paramètres','M12 12m-3 0a3 3 0 1 0 6 0 3 3 0 1 0-6 0M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42','Système'],
|
||
];
|
||
function ico(d){return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${d}"/></svg>`}
|
||
function toast(m,e){const t=$('T');t.textContent=m;t.className=e?'toast err':'toast';t.style.display='block';setTimeout(()=>t.style.display='none',3500)}
|
||
function esc(s){const d=document.createElement('div');d.textContent=s||'';return d.innerHTML}
|
||
function N(n){return new Intl.NumberFormat('fr-FR').format(n||0)}
|
||
function D(d){return d?new Date(d).toLocaleDateString('fr-FR',{day:'2-digit',month:'short',year:'numeric'}):''}
|
||
function md2h(t){return t.replace(/```[\w]*\n([\s\S]*?)```/g,'<pre>$1</pre>').replace(/\*\*(.*?)\*\*/g,'<strong>$1</strong>').replace(/`(.*?)`/g,'<code>$1</code>').replace(/\n\n/g,'<br><br>')}
|
||
async function api(p,o={}){const h={'Content-Type':'application/json'};if(TK)h['Authorization']='Bearer '+TK;const r=await fetch(API+p,{...o,headers:h});return r.json()}
|
||
async function eng(a){return(await fetch(V2E+'?action='+a+'&token=WEVADS2026')).json()}
|
||
async function brg(a){return(await fetch(BRG+'?action='+a+'&token=WEVADS2026')).json()}
|
||
function sR(){$('LF').style.display='none';$('RF').style.display=''}
|
||
function sL(){$('RF').style.display='none';$('LF').style.display=''}
|
||
async function doLogin(){const e=v('lE'),p=v('lP');if(!e||!p)return toast('Requis',1);$('bL').textContent='...';try{const r=await api('/auth/login',{method:'POST',body:JSON.stringify({email:e,password:p})});TK=r.token;U=r.user;enter()}catch(x){toast(x.message,1)}finally{$('bL').textContent='Se connecter'}}
|
||
async function doReg(){const n=v('rN'),e=v('rE'),p=v('rP');if(!n||!e||!p)return toast('Requis',1);try{const r=await api('/auth/register',{method:'POST',body:JSON.stringify({name:n,email:e,company:v('rC'),password:p})});TK=r.token;U=r.user;enter()}catch(x){toast(x.message,1)}}
|
||
function enter(){try{sessionStorage.setItem('wt',TK);sessionStorage.setItem('wu',JSON.stringify(U))}catch(e){}$('LW').style.display='none';$('SB').style.display='flex';$('MA').style.display='flex';$('sU').textContent=U.name||U.email;$('sA').textContent=(U.name||'W')[0].toUpperCase();$('sT').textContent=U.role||'user';rnav();go('dashboard')}
|
||
function logout(){TK='';U={};try{sessionStorage.clear()}catch(e){}$('LW').style.display='flex';$('SB').style.display='none';$('MA').style.display='none'}
|
||
function rnav(){let h='',s='';P.forEach(p=>{if(p[3]!==s){s=p[3];h+=`<div class="sb-sec">${s}</div>`}h+=`<div class="sb-it ${PG===p[0]?'on':''}" onclick="go('${p[0]}')">${ico(p[2])}<span>${p[1]}</span></div>`});$('SN').innerHTML=h}
|
||
function go(p){PG=p;$('PT').textContent=P.find(x=>x[0]===p)?.[1]||p;rnav();const c=$('C');c.innerHTML='<div style="text-align:center;padding:40px"><div class="spinner"></div></div>';c.scrollTop=0;(RR[p]||RR.dashboard)()}
|
||
const RR={};
|
||
|
||
// =============== DASHBOARD ===============
|
||
RR.dashboard=async()=>{let k={},t={},c=[];try{k=(await eng('dashboard')).kpis||{}}catch(e){}try{t=(await eng('tracking_stats')).stats||{}}catch(e){}try{c=(await api('/campaigns/list')).items||[]}catch(e){}const rs=(await eng('dashboard').catch(()=>({}))).recent_sends||[];
|
||
$('C').innerHTML=`<div class="stats"><div class="stat s-ac"><div class="st-l">Contacts</div><div class="st-v">${N(k.contacts_total)}</div><div class="st-s">${N(k.contacts_active)} actifs</div></div><div class="stat s-bl"><div class="st-l">Emails envoyés</div><div class="st-v">${N(k.emails_sent_total)}</div><div class="st-s">${k.emails_sent_today||0} today</div></div><div class="stat s-gn"><div class="st-l">Senders</div><div class="st-v">${N(k.senders_active)}</div><div class="st-s">${k.senders_warming||0} warming</div></div><div class="stat s-pu"><div class="st-l">Opens / Clicks</div><div class="st-v">${N(t.total_opens)}</div><div class="st-s">${N(t.total_clicks)} clicks</div></div><div class="stat s-or"><div class="st-l">Warmup</div><div class="st-v">${N(k.warmup_accounts)}</div></div><div class="stat s-tg"><div class="st-l">Canaux</div><div class="st-v">4</div><div class="st-s"><span class="up">Email+TG</span></div></div></div>
|
||
<div class="row2"><div class="card"><div class="card-h"><div class="card-t">Derniers envois</div><button class="btn btn-sm btn-ac" onclick="go('send')">+ Envoyer</button></div><table><thead><tr><th>Sender</th><th>To</th><th>Sujet</th><th>ISP</th><th>Date</th></tr></thead><tbody>${rs.slice(0,8).map(s=>`<tr><td style="font-family:var(--m);font-size:10px;color:var(--mu)">${esc((s.sender||'').split('@')[0])}</td><td style="font-family:var(--m);font-size:10px">${esc(s.recipient||'')}</td><td style="color:var(--wh);font-size:11px">${esc((s.subject||'').substring(0,30))}</td><td><span class="badge b-em">${esc(s.isp||'')}</span></td><td style="font-size:10px;color:var(--dm)">${(s.created_at||'').substring(0,16)}</td></tr>`).join('')||'<tr><td colspan="5" style="text-align:center;color:var(--dm);padding:24px">Aucun envoi</td></tr>'}</tbody></table></div>
|
||
<div><div class="card"><div class="card-t" style="margin-bottom:10px">Infra</div>${[['Campagnes',k.campaigns_total,'('+k.campaigns_active+' actives)'],['Domaines',k.domains_active,''],['Bounces',k.bounces,''],['Tracking',N(k.tracking_events),'events'],['Ethica HCPs',N(k.ethica_hcps),'pharma']].map(([l,v2,s])=>`<div style="display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid var(--bd)"><span style="font-size:11px;color:var(--mu)">${l}</span><span style="font-family:var(--m);font-size:11px;color:var(--wh)">${v2||0} <span style="color:var(--dm);font-size:9px">${s}</span></span></div>`).join('')}</div>
|
||
<div class="card"><div class="card-t" style="margin-bottom:8px">Actions</div><div style="display:grid;gap:5px"><button class="btn btn-sm btn-ac btn-full" onclick="go('send')">⚡ Envoyer</button><button class="btn btn-sm btn-gh btn-full" onclick="go('senders')">Senders</button><button class="btn btn-sm btn-gh btn-full" onclick="go('ai')">🧠 AI Assistant</button></div></div></div></div>`};
|
||
|
||
// =============== SEND ===============
|
||
RR.send=async()=>{let m=[];try{m=(await eng('send_methods')).items||[]}catch(e){}
|
||
$('C').innerHTML=`<div class="card" style="border-color:var(--acBd)"><div class="card-h"><div class="card-t">⚡ Composer & Envoyer</div></div>
|
||
<div class="row2"><div><div class="field"><label>Canal</label><div class="ch-sel"><div class="ch-p sel" data-c="email" onclick="this.classList.toggle('sel')">✉ Email</div><div class="ch-p" data-c="sms" onclick="this.classList.toggle('sel')">💬 SMS</div><div class="ch-p" data-c="whatsapp" onclick="this.classList.toggle('sel')">📱 WhatsApp</div><div class="ch-p" data-c="telegram" onclick="this.classList.toggle('sel')">✈ Telegram</div></div></div>
|
||
<div class="field"><label>Méthode</label><select id="sM">${m.map(x=>`<option value="${x.name}">⚙ ${x.name} — ${x.type}</option>`).join('')}</select></div><div class="field"><label>SMTP Config</label><select id="sC"><option value="">— Auto (Brain choisit) —</option></select></div>
|
||
<div class="field"><label>Destinataire(s)</label><input id="sTo" placeholder="email@example.com (virgules pour bulk)"></div>
|
||
<div class="field"><label>Objet</label><input id="sSu" placeholder="Objet du message"></div>
|
||
<div style="display:flex;gap:6px;margin:6px 0"><button class="btn btn-sm btn-gh" style="color:var(--pu);border-color:rgba(167,139,250,.2)" onclick="aiSubj()">🧠 Optimiser IA</button><span id="aiH" style="font-size:10px;color:var(--dm)"></span></div></div>
|
||
<div><div class="field"><label>From Name</label><input id="sFN" value="WEVAL Consulting"></div><div class="field"><label>From Email</label><input id="sFE" placeholder="Auto-select"></div>
|
||
<div style="display:flex;gap:6px;margin-top:14px"><button class="btn btn-ac" onclick="doSend()" style="flex:1">🧪 Test</button><button class="btn btn-gh" onclick="doBulk()" style="flex:1;color:var(--gn);border-color:rgba(52,211,153,.2)">🚀 Bulk</button></div></div></div>
|
||
<div class="field" style="margin-top:12px"><label>HTML</label><div style="display:flex;gap:6px;margin-bottom:6px"><button class="btn btn-sm btn-gh" onclick="aiBody()">🧠 IA</button><button class="btn btn-sm btn-gh" onclick="loadTpl()">📋 Template</button></div><textarea class="ed" id="sBo" rows="8" placeholder="<h1>Bonjour {{name}}</h1>"></textarea></div></div>
|
||
<div class="row2"><div class="card"><div class="card-t" style="margin-bottom:10px">File d'attente</div><div id="qS">...</div></div><div class="card"><div class="card-t" style="margin-bottom:10px">Tracking récent</div><div id="rT">...</div></div></div>`;
|
||
// Load SMTP configs
|
||
try{const sc=await brg('smtp_all');const sel=$('sC');(sc.smtp_configs||[]).forEach(c=>{const o=document.createElement('option');o.value=c.config_name;o.textContent=c.config_name+' — '+c.host+':'+c.port+' ('+Number(c.daily_limit).toLocaleString()+'/day)';sel.appendChild(o)})}catch(e){}
|
||
try{const q=await eng('queue');$('qS').innerHTML=`<div style="font-size:22px;font-weight:800;color:var(--wh);font-family:var(--m)">${q.pending||0}</div><div style="font-size:10px;color:var(--mu)">en attente</div>`}catch(e){$('qS').textContent='Offline'}
|
||
try{const t=await eng('tracking_stats');$('rT').innerHTML=(t.recent||[]).slice(0,5).map(r=>`<div style="display:flex;justify-content:space-between;padding:4px 0;border-bottom:1px solid var(--bd);font-size:10px"><span style="font-family:var(--m);color:var(--ac)">${esc((r.tracking_id||'').substring(0,18))}</span><span class="badge b-${r.event_type==='open'?'ok':'em'}">${r.event_type}</span></div>`).join('')||'—'}catch(e){}};
|
||
|
||
async function doSend(){const to=v('sTo'),su=v('sSu'),bo=v('sBo'),me=v('sM');if(!to||!su)return toast('To + Objet requis',1);toast('Envoi '+me+'...');try{const r=await fetch(V2E+'?action=send_test&token=WEVADS2026',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'to='+encodeURIComponent(to)+'&subject='+encodeURIComponent(su)+'&html='+encodeURIComponent(bo)+'&method='+me+'&from_name='+encodeURIComponent(v('sFN'))});const j=await r.json();toast(j.sent?'✅ Envoyé via '+me+' ('+j.tracking_id+')':'❌ '+(j.error||''),!j.sent)}catch(e){toast('Err: '+e.message,1)}}
|
||
async function doBulk(){const to=v('sTo'),su=v('sSu'),bo=v('sBo');const emails=to.split(',').map(e=>e.trim()).filter(Boolean);if(emails.length<2)return toast('Bulk = 2+ emails',1);toast('Bulk '+emails.length+'...');try{const r=await fetch(V2E+'?action=send_bulk&token=WEVADS2026',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({recipients:emails,subject:su,html:bo,method:v('sM'),from_name:v('sFN')})});const j=await r.json();toast(j.ok?'Bulk: '+emails.length+' emails':'Err',!j.ok)}catch(e){toast('Err',1)}}
|
||
async function aiSubj(){const s=v('sSu');if(!s)return toast('Sujet d\'abord',1);$('aiH').textContent='...';try{const r=await(await fetch(WEVIA,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:'Optimize this email subject for max open rate. Return ONLY the subject: '+s,mode:'fast'})})).json();$('sSu').value=r.response||s;$('aiH').textContent='✨ Optimisé'}catch(e){$('aiH').textContent='Err'}}
|
||
async function aiBody(){$('sBo').value='Génération...';try{const r=await(await fetch(WEVIA,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:'Generate a professional B2B HTML email about: '+(v('sSu')||'product launch')+'. Return ONLY HTML.',mode:'fast'})})).json();$('sBo').value=r.response||'Err'}catch(e){$('sBo').value='Err'}}
|
||
async function loadTpl(){try{const t=(await api('/templates/list')).items||[];if(t.length){$('sBo').value=t[0].html||'';toast(t[0].name+' chargé')}else toast('0 templates',1)}catch(e){toast('Err',1)}}
|
||
|
||
// =============== CAMPAIGNS ===============
|
||
let CMP=[];
|
||
RR.campaigns=async()=>{try{CMP=(await api('/campaigns/list')).items||[]}catch(e){CMP=[]}
|
||
$('C').innerHTML=`<div class="toolbar"><input type="search" id="cS" placeholder="Rechercher..." oninput="fCmp()"><div class="sp"></div><button class="btn btn-sm btn-ac" onclick="oCmp()">+ Campagne</button></div><div class="card" style="padding:0;overflow:hidden"><table><thead><tr><th>Nom</th><th>Sujet</th><th>Statut</th><th>Envoyés</th><th>Ouverts</th><th>Actions</th></tr></thead><tbody id="cT"></tbody></table></div>`;rCmp()};
|
||
function rCmp(f=''){const i=f?CMP.filter(c=>[c.name,c.subject].join(' ').toLowerCase().includes(f)):CMP;$('cT').innerHTML=i.map(c=>`<tr><td style="color:var(--wh);font-weight:600">${esc(c.name)}</td><td>${esc(c.subject)}</td><td><span class="badge b-${c.status==='sent'?'sent':c.status==='draft'?'ac':'wait'}">${c.status}</span></td><td style="font-family:var(--m)">${N(c.metrics?.sent)}</td><td style="font-family:var(--m)">${N(c.metrics?.opened)}</td><td><button class="btn btn-sm btn-gh" onclick="simS('${c.id}')">Sim</button></td></tr>`).join('')||'<tr><td colspan="6" style="text-align:center;color:var(--dm);padding:24px">—</td></tr>'}
|
||
function fCmp(){rCmp(v('cS').toLowerCase())}
|
||
async function simS(id){try{await api('/campaigns/'+id+'/send-simulate',{method:'POST'});toast('Simulé');RR.campaigns()}catch(e){toast(e.message,1)}}
|
||
function oCmp(){const m=document.createElement('div');m.className='modal-bg';m.onclick=e=>{if(e.target===m)m.remove()};m.innerHTML=`<div class="modal"><h3>Nouvelle campagne</h3><div class="field"><label>Nom</label><input id="nN"></div><div class="field"><label>Objet</label><input id="nS"></div><div class="field"><label>Audience</label><input type="number" id="nA" value="500"></div><div class="field"><label>HTML</label><textarea class="ed" id="nH" rows="4"></textarea></div><div class="brow"><button class="btn btn-gh" onclick="this.closest('.modal-bg').remove()">Annuler</button><button class="btn btn-ac" onclick="cCmp()">Créer</button></div></div>`;document.body.appendChild(m)}
|
||
async function cCmp(){try{await api('/campaigns',{method:'POST',body:JSON.stringify({name:v('nN'),subject:v('nS'),audience_size:+v('nA'),content_html:v('nH')})});document.querySelector('.modal-bg')?.remove();toast('Créée');RR.campaigns()}catch(e){toast(e.message,1)}}
|
||
|
||
// =============== CONTACTS ===============
|
||
let CTC=[];
|
||
RR.contacts=async()=>{try{CTC=(await api('/contacts/list')).items||[]}catch(e){CTC=[]}
|
||
$('C').innerHTML=`<div class="toolbar"><input type="search" id="ctS" placeholder="Rechercher..." oninput="fCtc()"><div class="sp"></div><button class="btn btn-sm btn-ac" onclick="oCtc()">+ Ajouter</button></div><div class="card" style="padding:0;overflow:hidden"><table><thead><tr><th>Nom</th><th>Email</th><th>Entreprise</th><th>Pays</th><th>Tags</th><th></th></tr></thead><tbody id="ctT"></tbody></table></div>`;rCtc()};
|
||
function rCtc(f=''){const i=f?CTC.filter(c=>[c.email,c.first_name,c.last_name].join(' ').toLowerCase().includes(f)):CTC;$('ctT').innerHTML=i.map(c=>`<tr><td style="color:var(--wh);font-weight:600">${esc(c.first_name||'')} ${esc(c.last_name||'')}</td><td style="font-family:var(--m);font-size:11px">${esc(c.email)}</td><td>${esc(c.company||'')}</td><td>${esc(c.country||'')}</td><td><div class="tags">${(c.tags||[]).map(t=>`<span class="tag">${esc(t)}</span>`).join('')}</div></td><td><button class="btn btn-sm btn-rd" onclick="dCtc('${c.id}')">×</button></td></tr>`).join('')||'<tr><td colspan="6" style="text-align:center;color:var(--dm);padding:24px">—</td></tr>'}
|
||
function fCtc(){rCtc(v('ctS').toLowerCase())}
|
||
async function dCtc(id){if(!confirm('Supprimer?'))return;try{await api('/contacts/'+id,{method:'DELETE'});toast('OK');RR.contacts()}catch(e){toast(e.message,1)}}
|
||
function oCtc(){const m=document.createElement('div');m.className='modal-bg';m.onclick=e=>{if(e.target===m)m.remove()};m.innerHTML=`<div class="modal"><h3>Contact</h3><div class="row2"><div class="field"><label>Prénom</label><input id="cF"></div><div class="field"><label>Nom</label><input id="cL"></div></div><div class="field"><label>Email</label><input id="cE"></div><div class="field"><label>Tél</label><input id="cP"></div><div class="row2"><div class="field"><label>Entreprise</label><input id="cCo"></div><div class="field"><label>Pays</label><input id="cCy"></div></div><div class="field"><label>Tags</label><input id="cTg" placeholder="vip, prospect"></div><div class="brow"><button class="btn btn-gh" onclick="this.closest('.modal-bg').remove()">×</button><button class="btn btn-ac" onclick="aCtc()">Ajouter</button></div></div>`;document.body.appendChild(m)}
|
||
async function aCtc(){try{await api('/contacts',{method:'POST',body:JSON.stringify({email:v('cE'),first_name:v('cF'),last_name:v('cL'),company:v('cCo'),phone:v('cP'),country:v('cCy'),tags:v('cTg').split(',').map(t=>t.trim()).filter(Boolean)})});document.querySelector('.modal-bg')?.remove();toast('Ajouté');RR.contacts()}catch(e){toast(e.message,1)}}
|
||
|
||
// =============== SENDERS ===============
|
||
RR.senders=async()=>{let d={};try{d=await brg('senders_full')}catch(e){}const pv=d.providers||[];const os=d.o365_status||[];const gs=d.gsuite_workspaces||[];const sm=d.smtp_configs||[];const wt=d.warmup_by_type||[];
|
||
$('C').innerHTML=`<div class="stats"><div class="stat s-ac"><div class="st-l">Total Senders</div><div class="st-v">${N(d.total_senders)}</div><div class="st-s">22 providers</div></div><div class="stat s-gn"><div class="st-l">Capacité/jour</div><div class="st-v">${N(d.total_capacity)}</div></div><div class="stat s-bl"><div class="st-l">SMTP Configs</div><div class="st-v">${sm.length}</div></div><div class="stat s-pu"><div class="st-l">GSuite WS</div><div class="st-v">${gs.length}</div></div></div>
|
||
<div class="row2"><div class="card"><div class="card-t" style="margin-bottom:12px">Senders par Provider</div><table><thead><tr><th>Provider</th><th>Comptes</th><th>Capacité/jour</th></tr></thead><tbody>${pv.map(p=>`<tr><td style="color:var(--wh);font-weight:600">${esc(p.provider)}</td><td style="font-family:var(--m)">${N(p.cnt)}</td><td style="font-family:var(--m);color:var(--gn)">${N(p.capacity)}</td></tr>`).join('')}</tbody></table></div>
|
||
<div><div class="card"><div class="card-t" style="margin-bottom:10px">O365 Status</div>${os.map(s=>`<div style="display:flex;justify-content:space-between;padding:5px 0;border-bottom:1px solid var(--bd)"><span style="font-size:11px;color:var(--wh)">${esc(s.status)}</span><span style="font-family:var(--m);font-size:12px;font-weight:700;color:var(--ac)">${N(s.cnt)}</span></div>`).join('')}</div>
|
||
<div class="card"><div class="card-t" style="margin-bottom:10px">GSuite Workspaces</div>${gs.map(g=>`<div style="display:flex;justify-content:space-between;padding:5px 0;border-bottom:1px solid var(--bd)"><span style="font-family:var(--m);font-size:10px;color:var(--wh)">${esc(g.domain)}</span><span style="font-size:10px;color:var(--mu)">${g.users_count} users</span><span class="badge b-ok">${esc(g.method)}</span></div>`).join('')||'<div style="color:var(--dm);font-size:11px">—</div>'}</div>
|
||
<div class="card"><div class="card-t" style="margin-bottom:10px">SMTP Configs (${sm.length})</div>${sm.slice(0,8).map(s=>`<div style="display:flex;justify-content:space-between;padding:4px 0;border-bottom:1px solid var(--bd)"><span style="font-size:10px;color:var(--wh)">${esc(s.config_name)}</span><span style="font-family:var(--m);font-size:9px;color:var(--mu)">${esc(s.host)}:${s.port}</span></div>`).join('')}</div></div></div>
|
||
<div class="card"><div class="card-t" style="margin-bottom:10px">Warmup par Type</div><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:8px">${wt.map(w=>`<div style="padding:10px;background:var(--sf);border:1px solid var(--bd);border-radius:var(--r);text-align:center"><div style="font-size:10px;color:var(--mu)">${esc(w.account_type)}</div><div style="font-family:var(--m);font-size:16px;font-weight:800;color:var(--wh)">${N(w.cnt)}</div><div style="font-size:9px;color:var(--gn)">${N(w.cap)}/day</div></div>`).join('')}</div></div>`};
|
||
|
||
// =============== WARMUP ===============
|
||
RR.warmup=async()=>{let d={};try{d=await brg('warmup')}catch(e){}const bt=d.by_type||[];
|
||
$('C').innerHTML=`<div class="stats"><div class="stat s-ac"><div class="st-l">Total</div><div class="st-v">${N(d.total)}</div></div><div class="stat s-gn"><div class="st-l">Actifs</div><div class="st-v">${d.active||0}</div></div><div class="stat s-bl"><div class="st-l">Ready</div><div class="st-v">${d.ready||0}</div></div></div>
|
||
<div class="card"><div class="card-t" style="margin-bottom:12px">Par type de compte</div><table><thead><tr><th>Type</th><th>Comptes</th><th>Capacité/jour</th><th>Envoyés today</th></tr></thead><tbody>${bt.map(t=>`<tr><td style="color:var(--wh);font-weight:600">${esc(t.account_type||'—')}</td><td style="font-family:var(--m)">${N(t.cnt)}</td><td style="font-family:var(--m);color:var(--gn)">${N(t.capacity)}</td><td style="font-family:var(--m);color:var(--ac)">${N(t.sent)}</td></tr>`).join('')||'<tr><td colspan="4" style="text-align:center;color:var(--dm)">—</td></tr>'}</tbody></table></div>
|
||
<div class="card"><div class="card-t" style="margin-bottom:8px">Pipeline Warmup</div><p style="font-size:12px;color:var(--mu);line-height:1.7">Escalade progressive: Phase 1 (10/jour) → Phase 2 (50/jour) → Phase 3 (200/jour) → Phase 4 (500/jour). Validation taux inbox entre chaque palier.</p></div>`};
|
||
|
||
// =============== DOMAINS ===============
|
||
RR.domains=async()=>{let d={};try{d=await brg('domains')}catch(e){}const doms=d.domains||[];
|
||
$('C').innerHTML=`<div class="stats"><div class="stat s-ac"><div class="st-l">Domaines</div><div class="st-v">${d.total||0}</div></div><div class="stat s-gn"><div class="st-l">Activés</div><div class="st-v">${doms.filter(x=>x.status==='Activated').length}</div></div></div>
|
||
<div class="card" style="padding:0;overflow:hidden"><table><thead><tr><th>Domaine</th><th>Statut</th><th>Dispo</th><th>Compte</th><th>Brand</th></tr></thead><tbody>${doms.map(d=>`<tr><td style="font-family:var(--m);color:var(--wh);font-weight:600">${esc(d.name)}</td><td><span class="badge b-${d.status==='Activated'?'ok':'wait'}">${d.status}</span></td><td>${esc(d.availability||'')}</td><td style="font-size:10px;color:var(--mu)">${esc(d.account_name||'')}</td><td>${d.has_brand==='Yes'?'✅':'—'}</td></tr>`).join('')}</tbody></table></div>`};
|
||
|
||
// =============== SEEDS ===============
|
||
RR.seeds=async()=>{let d={};try{d=await brg('seeds')}catch(e){}
|
||
$('C').innerHTML=`<div class="stats"><div class="stat s-ac"><div class="st-l">Seed Accounts</div><div class="st-v">${N(d.accounts)}</div></div><div class="stat s-bl"><div class="st-l">Inboxes</div><div class="st-v">${N(d.inboxes)}</div></div><div class="stat s-gn"><div class="st-l">Résultats</div><div class="st-v">${N(d.results)}</div></div></div>
|
||
<div class="card"><div class="card-t" style="margin-bottom:10px">Inbox Testing</div><p style="font-size:12px;color:var(--mu);line-height:1.7">Les seeds testent le placement inbox avant envoi réel. Chaque seed email vérifie inbox vs spam chez Gmail, Outlook, Yahoo, T-Online, GMX.</p><div style="margin-top:12px;display:flex;gap:6px"><button class="btn btn-sm btn-ac" onclick="toast('Seed test lancé')">Lancer test</button><button class="btn btn-sm btn-gh" onclick="toast('Check...')">Vérifier</button></div></div>`};
|
||
|
||
// =============== REPUTATION ===============
|
||
RR.reputation=async()=>{let d={};try{d=await brg('reputation')}catch(e){}
|
||
$('C').innerHTML=`<div class="stats"><div class="stat s-rd"><div class="st-l">Bounces</div><div class="st-v">${d.bounces||0}</div></div><div class="stat s-ac"><div class="st-l">Bounce Log</div><div class="st-v">${d.bounce_log||0}</div></div><div class="stat s-bl"><div class="st-l">Blacklisted</div><div class="st-v">${d.blacklisted||0}</div></div></div>
|
||
<div class="card"><div class="card-t" style="margin-bottom:10px">Blacklist Check</div><div style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px">${['Spamhaus','Barracuda','SORBS','UCEPROTECT'].map(n=>`<div style="padding:12px;background:var(--sf);border:1px solid var(--bd);border-radius:var(--r);text-align:center"><div style="font-size:9px;color:var(--mu)">${n}</div><div style="font-size:14px;font-weight:700;color:var(--gn);margin-top:4px">Clean</div></div>`).join('')}</div></div>
|
||
<div class="card"><div class="card-t" style="margin-bottom:8px">Monitoring</div><p style="font-size:12px;color:var(--mu);line-height:1.7">Surveillance bounces, blacklists, plaintes. Le Brain Engine ajuste volumes et IPs en fonction du score réputation.</p></div>`};
|
||
|
||
// =============== BRAIN IA ===============
|
||
RR.brain=async()=>{let d={};try{d=await brg('brain')}catch(e){}const mt=d.methods||[];
|
||
$('C').innerHTML=`<div class="stats"><div class="stat s-ac"><div class="st-l">Configs</div><div class="st-v">${N(d.configs)}</div></div><div class="stat s-gn"><div class="st-l">Winners</div><div class="st-v">${d.winners||0}</div></div><div class="stat s-pu"><div class="st-l">Learnings</div><div class="st-v">${N(d.learnings)}</div></div><div class="stat s-bl"><div class="st-l">IA Engine</div><div class="st-v" style="font-size:16px">LIVE</div><div class="st-s">Sovereign GPU</div></div></div>
|
||
<div class="row2"><div class="card"><div class="card-t" style="margin-bottom:10px">Send Methods (Hamid)</div><table><thead><tr><th>Méthode</th><th>Description</th><th>Best ISPs</th></tr></thead><tbody>${mt.map(m=>`<tr><td style="color:var(--wh);font-weight:600">${esc(m.method_name)}</td><td style="font-size:11px;color:var(--mu)">${esc(m.description)}</td><td style="font-family:var(--m);font-size:10px;color:var(--ac)">${esc(m.best_for_isps||'')}</td></tr>`).join('')}</tbody></table></div>
|
||
<div class="card"><div class="card-t" style="margin-bottom:10px">Stack IA</div>${[['vLLM GPU','S95:8000','qwen2.5-14B'],['Groq','API','llama-3.3-70b'],['Cerebras','API','qwen-3-235b'],['Ollama S204','CPU','9 modèles'],['Qdrant RAG','S95:6333','95 memcells']].map(([n,h,d])=>`<div style="display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid var(--bd)"><span style="font-size:11px;color:var(--wh)">${n}</span><span style="font-family:var(--m);font-size:9px;color:var(--mu)">${h}</span><span style="font-size:10px;color:var(--ac)">${d}</span></div>`).join('')}</div></div>`};
|
||
|
||
// =============== CREATIVE ===============
|
||
RR.creative=async()=>{let d={};try{d=await brg('creative')}catch(e){}const subs=d.top_subjects||[];
|
||
$('C').innerHTML=`<div class="stats"><div class="stat s-ac"><div class="st-l">Performances</div><div class="st-v">${N(d.performance)}</div></div><div class="stat s-bl"><div class="st-l">Traductions</div><div class="st-v">${d.translations||0}</div></div></div>
|
||
<div class="card"><div class="card-h"><div class="card-t">Creative Engine IA</div><button class="btn btn-sm btn-ac" onclick="toast('Génération...')">🧠 Générer</button></div><p style="font-size:12px;color:var(--mu);margin-bottom:14px">Analyse performance sujets/from/HTML. Génération variantes IA. Promotion winners automatique.</p>
|
||
${subs.length?`<table><thead><tr><th>Sujet</th><th>Open%</th><th>Click%</th></tr></thead><tbody>${subs.map(s=>`<tr><td style="color:var(--wh)">${esc((s.subject||'').substring(0,45))}</td><td style="font-family:var(--m);color:var(--gn)">${s.open_rate||0}%</td><td style="font-family:var(--m);color:var(--ac)">${s.click_rate||0}%</td></tr>`).join('')}</tbody></table>`:''}</div>`};
|
||
|
||
// =============== ANALYTICS ===============
|
||
RR.analytics=async()=>{let t={},d={};try{t=(await eng('tracking_stats')).stats||{}}catch(e){}try{d=await eng('deliverability')}catch(e){}const isps=(d.by_isp||[]).slice(0,10);
|
||
$('C').innerHTML=`<div class="stats"><div class="stat s-gn"><div class="st-l">Opens</div><div class="st-v">${N(t.total_opens)}</div></div><div class="stat s-bl"><div class="st-l">Clicks</div><div class="st-v">${N(t.total_clicks)}</div></div><div class="stat s-rd"><div class="st-l">Bounces</div><div class="st-v">${t.total_bounces||0}</div></div><div class="stat s-pu"><div class="st-l">Events</div><div class="st-v">${N(t.tracking_events)}</div></div></div>
|
||
<div class="card"><div class="card-t" style="margin-bottom:12px">Délivrabilité par ISP</div><table><thead><tr><th>ISP</th><th>Envoyés</th><th>Délivrés</th><th>Failed</th><th>Taux</th></tr></thead><tbody>${isps.map(i=>{const r=i.total>0?((i.delivered/i.total)*100).toFixed(1):'0';return`<tr><td style="color:var(--wh);font-weight:600;text-transform:uppercase">${esc(i.isp)}</td><td style="font-family:var(--m)">${N(i.total)}</td><td style="font-family:var(--m);color:var(--gn)">${N(i.delivered)}</td><td style="font-family:var(--m);color:var(--rd)">${i.failed||0}</td><td><span style="font-family:var(--m);font-weight:700;color:${+r>98?'var(--gn)':+r>95?'var(--ac)':'var(--or)'}">${r}%</span></td></tr>`}).join('')||'<tr><td colspan="5" style="text-align:center;color:var(--dm)">—</td></tr>'}</tbody></table></div>
|
||
<div class="card"><div class="card-t" style="margin-bottom:10px">Événements récents</div><table><thead><tr><th>ID</th><th>Type</th><th>IP</th><th>Date</th></tr></thead><tbody>${((await eng('tracking_stats').catch(()=>({}))).recent||[]).slice(0,8).map(e=>`<tr><td style="font-family:var(--m);font-size:10px;color:var(--ac)">${esc((e.tracking_id||'').substring(0,22))}</td><td><span class="badge b-${e.event_type==='open'?'ok':'em'}">${e.event_type}</span></td><td style="font-family:var(--m);font-size:10px;color:var(--mu)">${esc(e.ip_address||'')}</td><td style="font-size:10px;color:var(--dm)">${(e.created_at||'').substring(0,19)}</td></tr>`).join('')}</tbody></table></div>`};
|
||
|
||
// =============== AI ASSISTANT ===============
|
||
let chatH=[];
|
||
RR.ai=()=>{$('C').innerHTML=`<div class="chat-w"><div class="chat-m" id="aM"><div class="chat-e"><div style="font-size:36px">🧠</div><h3>WEVIA AI</h3><p style="font-size:12px;margin-top:6px;color:var(--mu)">Optimisez sujets, générez contenu, analysez délivrabilité.</p><div style="display:flex;gap:6px;justify-content:center;margin-top:12px;flex-wrap:wrap"><button class="btn btn-sm btn-gh" onclick="aQ('Optimise: Découvrez nos nouveautés')">Sujet</button><button class="btn btn-sm btn-gh" onclick="aQ('Email B2B pharma')">Email</button><button class="btn btn-sm btn-gh" onclick="aQ('Audit délivrabilité')">Audit</button></div></div></div><div class="chat-b"><textarea id="aI" rows="1" placeholder="Message..." onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();aS()}" oninput="this.style.height='42px';this.style.height=Math.min(this.scrollHeight,110)+'px'"></textarea><button class="btn btn-sm btn-ac" onclick="aS()">→</button></div></div>`};
|
||
function aQ(q){$('aI').value=q;aS()}
|
||
async function aS(){const msg=v('aI');if(!msg)return;const b=$('aM');b.querySelector('.chat-e')?.remove();b.innerHTML+=`<div class="chat-u">${esc(msg)}</div>`;$('aI').value='';const lid='l'+Date.now();b.innerHTML+=`<div class="chat-a" id="${lid}"><span class="ldot"></span><span class="ldot"></span><span class="ldot"></span></div>`;b.scrollTop=b.scrollHeight;try{chatH.push({role:'user',content:msg});const r=await(await fetch(WEVIA,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:msg,mode:'full',history:chatH.slice(-6)})})).json();const re=r.response||r.error||'Err';chatH.push({role:'assistant',content:re});$(lid).outerHTML=`<div class="chat-a">${md2h(re)}</div>`}catch(e){$(lid).outerHTML=`<div class="chat-a" style="color:var(--rd)">Err: ${e.message}</div>`}b.scrollTop=b.scrollHeight}
|
||
|
||
// =============== CHANNELS ===============
|
||
RR.channels=async()=>{let se={};try{se=await(await fetch(SE+'?action=status&token=ETHICA_API_2026_SECURE')).json()}catch(e){}const inf=se.infrastructure||{};const o=inf.o365_senders||{};
|
||
$('C').innerHTML=`<div class="stats"><div class="stat s-bl"><div class="st-l">Email</div><div class="st-v" style="font-size:16px;color:var(--em)">LIVE</div><div class="st-s">PMTA+${o.active||0} O365</div></div><div class="stat s-tg"><div class="st-l">Telegram</div><div class="st-v" style="font-size:16px;color:var(--tg)">LIVE</div></div><div class="stat s-gn"><div class="st-l">SMS</div><div class="st-v" style="font-size:16px;color:var(--or)">PENDING</div></div><div class="stat s-or"><div class="st-l">WhatsApp</div><div class="st-v" style="font-size:16px;color:var(--or)">PENDING</div></div></div>
|
||
<div class="card"><div class="card-h"><div class="card-t">✈ Telegram</div><span class="badge b-ok">Connecté</span></div><p style="font-size:12px;color:var(--mu);margin-bottom:10px">@wevads_alerts_bot → Yacine</p><button class="btn btn-sm btn-gh" style="color:var(--tg);border-color:rgba(34,158,217,.2)" onclick="tgTest()">Test Telegram</button></div>
|
||
<div class="card"><div class="card-h"><div class="card-t">💬 SMS OVH</div><span class="badge b-wait">Credentials</span></div><div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px"><div class="field"><label>App Key</label><input id="sk1"></div><div class="field"><label>Secret</label><input id="sk2"></div><div class="field"><label>Consumer</label><input id="sk3"></div></div><button class="btn btn-sm btn-gh" style="color:var(--sm)" onclick="cfgSMS()">Configurer</button></div>
|
||
<div class="card"><div class="card-h"><div class="card-t">📱 WhatsApp</div><span class="badge b-wait">Credentials</span></div><div class="row2"><div class="field"><label>Phone ID</label><input id="wk1"></div><div class="field"><label>Token</label><input id="wk2"></div></div><button class="btn btn-sm btn-gh" style="color:var(--wa)" onclick="cfgWA()">Configurer</button></div>`};
|
||
async function tgTest(){toast('Envoi TG...');try{const r=await(await fetch('https://api.telegram.org/bot8544624912:AAEm9ttXK6JeFqAL-gcvB5sreCBhXzzQwrs/sendMessage',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({chat_id:'7605775322',text:'✅ WEVADS IA v3.0 Test\n'+new Date().toLocaleString('fr-FR')})})).json();toast(r.ok?'TG OK':'Err',!r.ok)}catch(e){toast('Err',1)}}
|
||
async function cfgSMS(){toast('Config SMS...')}
|
||
async function cfgWA(){toast('Config WA...')}
|
||
|
||
// =============== TEMPLATES ===============
|
||
let TPL=[];
|
||
RR.templates=async()=>{try{TPL=(await api('/templates/list')).items||[]}catch(e){TPL=[]}
|
||
$('C').innerHTML=`<div class="toolbar"><div class="sp"></div><button class="btn btn-sm btn-ac" onclick="oTpl()">+ Nouveau</button></div><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:14px" id="tG"></div>`;
|
||
$('tG').innerHTML=TPL.map(t=>`<div class="card" style="cursor:pointer"><div style="display:flex;justify-content:space-between;margin-bottom:8px"><span style="font-weight:700;color:var(--wh)">${esc(t.name)}</span><span class="badge b-ok">${esc(t.category)}</span></div><div style="background:var(--bg);border:1px solid var(--bd);border-radius:var(--r3);padding:10px;min-height:50px;font-size:10px;color:var(--mu);overflow:hidden;max-height:70px">${t.html?t.html.substring(0,120)+'...':'—'}</div></div>`).join('')||'<div style="text-align:center;color:var(--dm);padding:30px;grid-column:1/-1">—</div>'};
|
||
function oTpl(){const m=document.createElement('div');m.className='modal-bg';m.onclick=e=>{if(e.target===m)m.remove()};m.innerHTML=`<div class="modal"><h3>Template</h3><div class="field"><label>Nom</label><input id="tN"></div><div class="field"><label>Cat</label><select id="tC"><option>marketing</option><option>transactional</option><option>newsletter</option></select></div><div class="field"><label>HTML</label><textarea class="ed" id="tH" rows="6"></textarea></div><div class="brow"><button class="btn btn-gh" onclick="this.closest('.modal-bg').remove()">×</button><button class="btn btn-ac" onclick="sTpl()">Save</button></div></div>`;document.body.appendChild(m)}
|
||
async function sTpl(){try{await api('/templates',{method:'POST',body:JSON.stringify({name:v('tN'),category:v('tC'),html:v('tH')})});document.querySelector('.modal-bg')?.remove();toast('Créé');RR.templates()}catch(e){toast(e.message,1)}}
|
||
|
||
// =============== SEND ENGINE ===============
|
||
RR.sendengine=async()=>{let se={},k={},w={};try{se=await(await fetch(SE+'?action=status&token=ETHICA_API_2026_SECURE')).json()}catch(e){}try{k=(await eng('dashboard')).kpis||{}}catch(e){}try{w=await eng('warmup_status')}catch(e){}const inf=se.infrastructure||{};const o=inf.o365_senders||{};const pm=inf.pmta||{};const cl=se.clients||{};
|
||
$('C').innerHTML=`<div class="stats"><div class="stat s-ac"><div class="st-l">Méthode</div><div class="st-v" style="font-size:14px">${(inf.method||'—').toUpperCase()}</div></div><div class="stat s-bl"><div class="st-l">O365</div><div class="st-v">${o.active||0}/${o.total||0}</div></div><div class="stat s-gn"><div class="st-l">PMTA</div><div class="st-v">${pm.servers_alive||0}/${pm.servers_configured||0}</div></div><div class="stat s-pu"><div class="st-l">Warmup</div><div class="st-v">${N(w.total_accounts||k.warmup_accounts)}</div></div><div class="stat s-or"><div class="st-l">Queue</div><div class="st-v">${k.queue_pending||0}</div></div></div>
|
||
<div class="row2"><div class="card"><div class="card-t" style="margin-bottom:10px">Clients</div>${[['Ethica (Pharma)',cl.ethica?.contacts,cl.ethica?.emails,'var(--ac)'],['WEVADS (Multi)',cl.wevads?.contacts,k.senders_active,'var(--cy)']].map(([n,c,e,col])=>`<div style="padding:12px;background:var(--sf);border:1px solid var(--bd);border-radius:var(--r);margin-bottom:8px"><div style="font-size:11px;font-weight:600;color:${col}">${n}</div><div style="font-family:var(--m);font-size:18px;font-weight:800;color:var(--wh)">${N(c||0)}</div><div style="font-size:10px;color:var(--mu)">${N(e||0)} active</div></div>`).join('')}</div>
|
||
<div class="card"><div class="card-t" style="margin-bottom:10px">Métriques</div><div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">${[['Envoyés',k.emails_sent_total,'var(--wh)'],['Today',k.emails_sent_today,'var(--gn)'],['Bounces',k.bounces,'var(--rd)'],['Methods',k.send_methods,'var(--cy)']].map(([l,v2,c])=>`<div style="text-align:center;padding:10px;background:var(--sf);border:1px solid var(--bd);border-radius:var(--r)"><div style="font-size:16px;font-weight:800;color:${c};font-family:var(--m)">${N(v2||0)}</div><div style="font-size:9px;color:var(--mu);margin-top:3px">${l}</div></div>`).join('')}</div></div></div>`};
|
||
|
||
|
||
|
||
// =============== OFFERS ===============
|
||
RR.offers=async()=>{let d={};try{d=await brg('offers')}catch(e){}const off=d.offers||[];const sp=d.sponsors||[];
|
||
$('C').innerHTML=`<div class="stats"><div class="stat s-ac"><div class="st-l">Offres</div><div class="st-v">${N(d.total_offers)}</div></div><div class="stat s-gn"><div class="st-l">Sponsors</div><div class="st-v">${sp.length}</div></div><div class="stat s-bl"><div class="st-l">Networks</div><div class="st-v">${(d.networks||[]).length}</div></div></div>
|
||
<div class="card"><div class="card-h"><div class="card-t">Catalogue Offres (${N(d.total_offers)})</div><button class="btn btn-sm btn-ac" onclick="toast('Import depuis CX3/Double M')">+ Importer</button></div>
|
||
<table><thead><tr><th>Offre</th><th>Pays</th><th>Vertical</th><th>Payout</th><th>Statut</th><th>Clicks</th><th>Conv.</th></tr></thead>
|
||
<tbody>${off.slice(0,20).map(o=>\`<tr><td style="color:var(--wh);font-weight:500;font-size:11px">\${esc((o.offer_name||'').substring(0,45))}</td><td><span class="badge b-em">\${esc(o.country_code)}</span></td><td style="font-size:11px;color:var(--mu)">\${esc(o.vertical)}</td><td style="font-family:var(--m);color:var(--gn)">\${o.payout>0?'$'+o.payout:'—'}</td><td><span class="badge b-\${o.status==='active'?'ok':o.status==='public'?'ac':'wait'}">\${esc(o.status)}</span></td><td style="font-family:var(--m)">\${N(o.clicks)}</td><td style="font-family:var(--m);color:var(--ac)">\${N(o.conversions)}</td></tr>\`).join('')}</tbody></table></div>`;
|
||
};
|
||
|
||
// =============== SPONSORS ===============
|
||
RR.sponsors=async()=>{let d={};try{d=await brg('offers')}catch(e){}const sp=d.sponsors||[];const nw=d.networks||[];
|
||
$('C').innerHTML=`<div class="stats"><div class="stat s-ac"><div class="st-l">Sponsors</div><div class="st-v">${sp.length}</div></div><div class="stat s-gn"><div class="st-l">Networks</div><div class="st-v">${nw.length}</div></div><div class="stat s-bl"><div class="st-l">Offres</div><div class="st-v">${N(d.total_offers)}</div></div></div>
|
||
<div class="row2"><div class="card"><div class="card-h"><div class="card-t">Sponsors</div><button class="btn btn-sm btn-ac" onclick="oSpon()">+ Ajouter</button></div>
|
||
<table><thead><tr><th>Nom</th><th>Type</th><th>Code</th><th>Volume/mois</th><th>Contact</th><th>Actif</th></tr></thead>
|
||
<tbody>${sp.map(s=>\`<tr><td style="color:var(--wh);font-weight:600">\${esc(s.name)}</td><td><span class="badge b-\${s.type==='b2b_sponsor'?'ac':'em'}">\${esc(s.type)}</span></td><td style="font-family:var(--m);font-size:10px">\${esc(s.sponsor_code||'—')}</td><td style="font-family:var(--m)">\${N(s.monthly_volume)}</td><td style="font-size:10px;color:var(--mu)">\${esc(s.contact_email||'—')}</td><td>\${s.is_active?'✅':'❌'}</td></tr>\`).join('')}</tbody></table></div>
|
||
<div><div class="card"><div class="card-t" style="margin-bottom:10px">Networks</div>${nw.map(n=>\`<div style="display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid var(--bd)"><span style="color:var(--wh)">\${esc(n.name)}</span><span class="badge b-\${n.status==='active'?'ok':'wait'}">\${n.status}</span></div>\`).join('')||'<div style="color:var(--dm)">—</div>'}</div>
|
||
<div class="card"><div class="card-t" style="margin-bottom:8px">Multi-client</div><p style="font-size:11px;color:var(--mu);line-height:1.6">Chaque sponsor a son propre pool d'offres, budget mensuel, et tracking séparé. Les offres sont attribuées par vertical et pays.</p>
|
||
<div style="margin-top:10px;display:grid;gap:5px"><button class="btn btn-sm btn-ac btn-full" onclick="oSpon()">Ajouter Sponsor</button><button class="btn btn-sm btn-gh btn-full" onclick="toast('Import réseau')">Importer Network</button></div></div></div></div>`;
|
||
};
|
||
|
||
function oSpon(){const m=document.createElement('div');m.className='modal-bg';m.onclick=e=>{if(e.target===m)m.remove()};
|
||
m.innerHTML=\`<div class="modal"><h3>Nouveau Sponsor</h3>
|
||
<div class="field"><label>Nom</label><input id="spN" placeholder="Nom du sponsor"></div>
|
||
<div class="row2"><div class="field"><label>Type</label><select id="spT"><option value="external">External</option><option value="b2b_sponsor">B2B Sponsor</option><option value="affiliate">Affiliate</option></select></div><div class="field"><label>Code</label><input id="spC" placeholder="ethica"></div></div>
|
||
<div class="field"><label>Email contact</label><input id="spE" placeholder="contact@sponsor.com"></div>
|
||
<div class="row2"><div class="field"><label>Volume/mois</label><input type="number" id="spV" value="20000"></div><div class="field"><label>Budget mensuel</label><input type="number" id="spB" value="0"></div></div>
|
||
<div class="brow"><button class="btn btn-gh" onclick="this.closest('.modal-bg').remove()">Annuler</button><button class="btn btn-ac" onclick="document.querySelector('.modal-bg')?.remove();toast('Sponsor ajouté')">Créer</button></div></div>\`;
|
||
document.body.appendChild(m)}
|
||
|
||
// =============== SERVERS ===============
|
||
RR.servers=async()=>{let d={};try{d=await brg('servers')}catch(e){}const mgmt=d.management_servers||[];const prov=d.providers||[];const cloud=d.cloud_accounts||[];
|
||
$('C').innerHTML=`<div class="stats"><div class="stat s-ac"><div class="st-l">MTA Servers</div><div class="st-v">${d.mta_servers||0}</div></div><div class="stat s-gn"><div class="st-l">PMTA Configs</div><div class="st-v">${d.pmta_configs||0}</div></div><div class="stat s-bl"><div class="st-l">VMTAs</div><div class="st-v">${d.vmtas||0}</div></div><div class="stat s-pu"><div class="st-l">SMTP Servers</div><div class="st-v">${d.smtp_servers||0}</div></div></div>
|
||
<div class="row2"><div class="card"><div class="card-h"><div class="card-t">Management Servers</div><button class="btn btn-sm btn-ac" onclick="oSrv()">+ Nouveau</button></div>
|
||
<table><thead><tr><th>Nom</th><th>IP</th><th>Provider</th><th>Type</th><th>SSH</th><th>Statut</th></tr></thead>
|
||
<tbody>${mgmt.map(s=>\`<tr><td style="color:var(--wh);font-weight:600">\${esc(s.name)}</td><td style="font-family:var(--m);font-size:10px">\${esc(s.main_ip)}</td><td>\${esc(s.provider_name)}</td><td><span class="badge b-em">\${esc(s.server_type||'mta')}</span></td><td style="font-family:var(--m);font-size:9px">\${esc(s.ssh_username)}@:\${s.ssh_port}</td><td><span class="badge b-\${s.status==='Activated'?'ok':'wait'}">\${s.status}</span></td></tr>\`).join('')||'<tr><td colspan="6" style="text-align:center;color:var(--dm);padding:20px">Aucun serveur</td></tr>'}</tbody></table></div>
|
||
<div><div class="card"><div class="card-t" style="margin-bottom:10px">Cloud Providers</div>${prov.map(p=>\`<div style="display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid var(--bd)"><span style="color:var(--wh);font-size:12px">\${esc(p.name)}</span><span class="badge b-\${p.status==='Activated'?'ok':'wait'}">\${p.status}</span></div>\`).join('')}</div>
|
||
<div class="card"><div class="card-t" style="margin-bottom:10px">Comptes Cloud</div>${cloud.map(c=>\`<div style="display:flex;justify-content:space-between;padding:5px 0;border-bottom:1px solid var(--bd)"><span style="font-size:11px;color:var(--wh)">\${esc(c.provider)}</span><span style="font-family:var(--m);font-size:12px;font-weight:700;color:var(--ac)">\${c.count}</span></div>\`).join('')}</div>
|
||
<div class="card"><div class="card-t" style="margin-bottom:8px">Créer serveur</div><p style="font-size:11px;color:var(--mu);line-height:1.6;margin-bottom:10px">Provisionner un nouveau VPS avec PMTA pré-installé. Choisir le provider, la région, et le type d'installation.</p>
|
||
<div style="display:grid;gap:6px"><button class="btn btn-sm btn-ac btn-full" onclick="oSrv()">Huawei Cloud ECS</button><button class="btn btn-sm btn-gh btn-full" onclick="oSrv()">Hetzner Cloud</button><button class="btn btn-sm btn-gh btn-full" onclick="oSrv()">Digital Ocean</button><button class="btn btn-sm btn-gh btn-full" onclick="oSrv()">Scaleway</button></div></div></div></div>`;
|
||
};
|
||
|
||
function oSrv(){const m=document.createElement('div');m.className='modal-bg';m.onclick=e=>{if(e.target===m)m.remove()};
|
||
m.innerHTML=\`<div class="modal"><h3>Nouveau Serveur MTA</h3>
|
||
<div class="field"><label>Provider</label><select id="svP"><option value="huawei">Huawei Cloud ECS</option><option value="hetzner">Hetzner Cloud</option><option value="digitalocean">Digital Ocean</option><option value="scaleway">Scaleway</option><option value="linode">Linode</option><option value="custom">Custom (SSH)</option></select></div>
|
||
<div class="field"><label>Nom</label><input id="svN" placeholder="MTA-EU-01"></div>
|
||
<div class="row2"><div class="field"><label>IP / Hostname</label><input id="svIP" placeholder="xxx.xxx.xxx.xxx"></div><div class="field"><label>SSH Port</label><input id="svPort" value="22"></div></div>
|
||
<div class="row2"><div class="field"><label>SSH User</label><input id="svUser" value="root"></div><div class="field"><label>SSH Password</label><input type="password" id="svPass"></div></div>
|
||
<div class="field"><label>Installation</label><select id="svInst"><option value="pmta">PMTA v5.0r3 + Domain</option><option value="pmta6">PMTA v6.0r4</option><option value="kumo">KumoMTA (OSS)</option><option value="postfix">Postfix MTA</option><option value="none">Aucune (SSH seul)</option></select></div>
|
||
<div class="field"><label>Domaine(s) associé(s)</label><input id="svDom" placeholder="send.wevads.com, mail.weval.ma"></div>
|
||
<div class="brow"><button class="btn btn-gh" onclick="this.closest('.modal-bg').remove()">Annuler</button><button class="btn btn-ac" onclick="createSrv()">Créer & Installer</button></div></div>\`;
|
||
document.body.appendChild(m)}
|
||
|
||
async function createSrv(){toast('Création serveur en cours... (CX relay)');document.querySelector('.modal-bg')?.remove();
|
||
// This would trigger via CX relay to S95 for actual server provisioning
|
||
toast('Serveur ajouté — installation PMTA en cours via Droid',false)}
|
||
|
||
// =============== INFRA ===============
|
||
RR.infra=async()=>{let d={};try{d=await brg('infra')}catch(e){}const sc=d.schemas||[];
|
||
$('C').innerHTML=`<div class="stats"><div class="stat s-ac"><div class="st-l">Tables</div><div class="st-v">${d.tables||0}</div></div><div class="stat s-bl"><div class="st-l">DB Size</div><div class="st-v" style="font-size:16px">${d.db_size||'?'}</div></div><div class="stat s-gn"><div class="st-l">APIs</div><div class="st-v">344</div></div><div class="stat s-pu"><div class="st-l">GOLD</div><div class="st-v">1,411</div></div></div>
|
||
<div class="row2"><div class="card"><div class="card-t" style="margin-bottom:10px">Schemas PostgreSQL</div><table><thead><tr><th>Schema</th><th>Tables</th></tr></thead><tbody>${sc.map(s=>`<tr><td style="color:var(--wh);font-weight:600">${esc(s.schemaname)}</td><td style="font-family:var(--m)">${s.cnt}</td></tr>`).join('')}</tbody></table></div>
|
||
<div class="card"><div class="card-t" style="margin-bottom:10px">Services</div>${[['S95 Apache','ADX 5821+BCG 5823+FMG 5822','ok'],['S95 PMTA v5.0r3','Port 25','ok'],['S95 Postfix','2525/2526','ok'],['S204 Nginx','HTTPS proxy','ok'],['Node.js V2','Port 5850','ok'],['S151 Tracking','HTTP relay','ok'],['Telegram Bot','@wevads_alerts_bot','ok'],['Qdrant RAG','S95:6333','ok'],['n8n Workflows','S95:5678','ok']].map(([n,d,s])=>`<div style="display:flex;justify-content:space-between;padding:5px 0;border-bottom:1px solid var(--bd)"><span style="font-size:11px;color:var(--wh)">${n}</span><span style="font-family:var(--m);font-size:9px;color:var(--mu)">${d}</span><span class="badge b-${s}">${s}</span></div>`).join('')}</div></div>`};
|
||
|
||
// =============== SETTINGS ===============
|
||
RR.settings=()=>{$('C').innerHTML=`<div class="card"><div class="card-t" style="margin-bottom:14px">Profil</div><div class="row2"><div class="field"><label>Nom</label><input value="${esc(U.name||'')}"></div><div class="field"><label>Email</label><input value="${esc(U.email||'')}" readonly style="opacity:.5"></div></div><button class="btn btn-sm btn-ac" style="margin-top:10px" onclick="toast('OK')">Save</button></div>
|
||
<div class="card"><div class="card-t" style="margin-bottom:10px">Auth DNS</div><table><tbody>${[['SPF','v=spf1 include:spf.pmta.weval ~all'],['DKIM','weval2026._domainkey'],['DMARC','p=quarantine'],['TLS','1.3 Enforced']].map(([k,v2])=>`<tr><td style="color:var(--wh)">${k}</td><td style="font-family:var(--m);font-size:11px;color:var(--mu)">${v2}</td></tr>`).join('')}</tbody></table></div>`};
|
||
|
||
// =============== INIT ===============
|
||
try{const st=sessionStorage.getItem('wt'),su=sessionStorage.getItem('wu');if(st&&su){TK=st;U=JSON.parse(su);enter()}}catch(e){}
|
||
fetch(API+'/health').then(r=>r.json()).then(j=>{if(j.status==='ok')$('TL').innerHTML='● Live — '+j.version}).catch(()=>{});
|
||
</script></body></html>
|
||
GEMINI_KEY=AIzaSyCMbYKyTldlCjg2eSCtwNONX9mMomhmIM4
|