301 lines
28 KiB
HTML
301 lines
28 KiB
HTML
<!DOCTYPE html><html lang="fr"><head>
|
||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate"><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||
<title>WEVADS - Warming Engine — WEVADS</title>
|
||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root{--bg:#080b12;--surface:#0f1420;--surface2:#161d2e;--border:rgba(255,255,255,.06);--text:#c8d0e0;--text-dim:#6b7a94;--text-bright:#edf0f7;--green:#00e68a;--red:#ff4d6a;--amber:#ffb547;--blue:#3d8bfd;--purple:#a78bfa;--cyan:#22d3ee;--orange:#f97316;--mono:'JetBrains Mono',monospace;--sans:'DM Sans',sans-serif}
|
||
*{margin:0;padding:0;box-sizing:border-box}
|
||
body{font-family:var(--sans);background:#060a14;color:var(--text);min-height:100vh}
|
||
.app{max-width:1480px;margin:0 auto;padding:20px}
|
||
.hdr{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;padding-bottom:14px;border-bottom:1px solid rgba(255,255,255,.05)}
|
||
.hdr h1{font-size:22px;font-weight:700;display:flex;align-items:center;gap:10px}
|
||
.hdr h1 span{color:var(--orange)}
|
||
.hdr-right{display:flex;align-items:center;gap:10px}
|
||
.pulse{width:8px;height:8px;border-radius:50%;background:var(--green);animation:pulse 2s infinite}
|
||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
|
||
.status-text{font-size:11px;color:var(--text-dim);font-family:var(--mono)}
|
||
.tabs{display:flex;gap:4px;margin-bottom:20px;background:var(--surface);border-radius:10px;padding:4px;width:fit-content}
|
||
.tab{padding:8px 18px;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;color:var(--text-dim);transition:.2s;border:none;background:none}
|
||
.tab:hover{color:var(--text)}
|
||
.tab.active{background:var(--surface2);color:var(--orange);box-shadow:0 2px 8px rgba(249,115,22,.15)}
|
||
.panel{display:none}.panel.active{display:block}
|
||
.g{display:grid;gap:14px;margin-bottom:18px}
|
||
.g5{grid-template-columns:repeat(5,1fr)}.g4{grid-template-columns:repeat(4,1fr)}.g3{grid-template-columns:repeat(3,1fr)}.g2{grid-template-columns:1fr 1fr}.g32{grid-template-columns:3fr 2fr}
|
||
.card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:18px;transition:.2s}
|
||
.card:hover{border-color:rgba(249,115,22,.15)}
|
||
.card-title{font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-dim);margin-bottom:10px;display:flex;align-items:center;gap:6px}
|
||
.kpi{text-align:center;padding:12px 8px}
|
||
.kpi-val{font-family:var(--mono);font-size:28px;font-weight:700;line-height:1}
|
||
.kpi-label{font-size:10px;color:var(--text-dim);margin-top:4px;text-transform:uppercase;letter-spacing:.5px}
|
||
.btn{padding:8px 16px;border:none;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;color:#fff;transition:.2s;font-family:var(--sans);display:inline-flex;align-items:center;gap:6px}
|
||
.btn:hover{transform:translateY(-1px);filter:brightness(1.15)}
|
||
.btn:active{transform:translateY(0)}
|
||
.btn-orange{background:linear-gradient(135deg,var(--orange),#ea580c)}
|
||
.btn-green{background:linear-gradient(135deg,var(--green),#059669)}
|
||
.btn-blue{background:linear-gradient(135deg,var(--blue),#2563eb)}
|
||
.btn-red{background:linear-gradient(135deg,var(--red),#dc2626)}
|
||
.btn-ghost{background:var(--surface2);border:1px solid var(--border);color:var(--text)}
|
||
.btn-sm{padding:5px 10px;font-size:11px}
|
||
table{width:100%;border-collapse:collapse;font-size:12px}
|
||
th{text-align:left;padding:10px 12px;color:var(--text-dim);font-size:10px;text-transform:uppercase;border-bottom:1px solid rgba(255,255,255,.06);letter-spacing:.5px;font-weight:600}
|
||
td{padding:9px 12px;border-bottom:1px solid rgba(255,255,255,.03)}
|
||
tr:hover td{background:rgba(249,115,22,.02)}
|
||
.badge{padding:2px 8px;border-radius:6px;font-size:10px;font-weight:600;display:inline-block}
|
||
.b-warm{background:rgba(249,115,22,.12);color:var(--orange)}
|
||
.b-grad{background:rgba(0,230,138,.12);color:var(--green)}
|
||
.b-pend{background:rgba(167,139,250,.12);color:var(--purple)}
|
||
.b-pause{background:rgba(255,77,106,.12);color:var(--red)}
|
||
.b-active{background:rgba(61,139,253,.12);color:var(--blue)}
|
||
.bar{height:6px;border-radius:3px;background:var(--surface2);overflow:hidden;flex:1}
|
||
.bar-fill{height:100%;border-radius:3px;transition:.5s}
|
||
.progress-row{display:flex;align-items:center;gap:8px;margin:4px 0}
|
||
.progress-label{font-size:10px;color:var(--text-dim);width:70px;text-align:right}
|
||
.progress-val{font-family:var(--mono);font-size:10px;width:40px;color:var(--text)}
|
||
.chart-container{position:relative;height:200px;padding:10px 0}
|
||
.chart-bar-row{display:flex;align-items:flex-end;gap:3px;height:160px;padding:0 8px}
|
||
.chart-bar{flex:1;background:linear-gradient(to top,rgba(249,115,22,.6),rgba(249,115,22,.2));border-radius:3px 3px 0 0;transition:.3s;position:relative;min-width:8px;cursor:pointer}
|
||
.chart-bar:hover{background:linear-gradient(to top,rgba(249,115,22,.9),rgba(249,115,22,.4))}
|
||
.chart-bar .tip{display:none;position:absolute;bottom:105%;left:50%;transform:translateX(-50%);background:var(--surface2);border:1px solid var(--border);padding:4px 8px;border-radius:6px;font-size:10px;white-space:nowrap;z-index:10}
|
||
.chart-bar:hover .tip{display:block}
|
||
.chart-labels{display:flex;gap:3px;padding:4px 8px;margin-top:4px}
|
||
.chart-labels span{flex:1;text-align:center;font-size:9px;color:var(--text-dim)}
|
||
.schedule-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(90px,1fr));gap:6px}
|
||
.schedule-item{background:var(--surface2);border-radius:8px;padding:10px;text-align:center;border:1px solid var(--border)}
|
||
.schedule-day{font-family:var(--mono);font-size:16px;font-weight:700;color:var(--orange)}
|
||
.schedule-mult{font-size:10px;color:var(--text-dim);margin-top:2px}
|
||
.filter-row{display:flex;gap:8px;margin-bottom:14px;align-items:center;flex-wrap:wrap}
|
||
.filter-row select,.filter-row input{background:var(--surface2);border:1px solid var(--border);border-radius:8px;padding:7px 12px;color:var(--text);font-size:12px;font-family:var(--sans)}
|
||
.log-box{font-family:var(--mono);font-size:11px;background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:12px;max-height:220px;overflow-y:auto;line-height:1.8}
|
||
.log-ok{color:var(--green)}.log-warn{color:var(--amber)}.log-err{color:var(--red)}.log-info{color:var(--cyan)}.log-ts{color:var(--text-dim)}
|
||
.toast{position:fixed;top:20px;right:20px;padding:12px 20px;border-radius:10px;font-size:13px;font-weight:500;z-index:9999;animation:slideIn .3s ease;display:none}
|
||
.toast.show{display:block}
|
||
.toast-ok{background:#060a14;color:#6ee7b7;border:1px solid #060a14}
|
||
.toast-err{background:#7f1d1d;color:#fca5a5;border:1px solid #ef4444}
|
||
.toast-info{background:#1e3a5f;color:#93c5fd;border:1px solid #3b82f6}
|
||
@keyframes slideIn{from{transform:translateX(100px);opacity:0}to{transform:translateX(0);opacity:1}}
|
||
.empty{text-align:center;padding:40px;color:var(--text-dim);font-size:13px}
|
||
@media(max-width:1100px){.g5{grid-template-columns:repeat(3,1fr)}.g4{grid-template-columns:repeat(2,1fr)}}
|
||
@media(max-width:768px){.g5,.g4,.g3,.g2,.g32{grid-template-columns:1fr}.tabs{width:100%;overflow-x:auto}}
|
||
.wv-status{position:fixed;top:12px;right:140px;z-index:9998;background:rgba(52,211,153,.15);border:1px solid #34d399;border-radius:12px;padding:3px 10px;color:#34d399;font-size:10px;font-weight:700;font-family:'JetBrains Mono',monospace}
|
||
</style><link rel="stylesheet" href="wevads-global.css?v1770777318">
|
||
</head><body>
|
||
|
||
|
||
<div class="app">
|
||
<div class="hdr">
|
||
<h1>🔥 <span>Warming</span> Engine</h1>
|
||
<div class="hdr-right"><div class="pulse"></div><span class="status-text" id="hdrStatus">Connecting...</span><button class="btn btn-ghost btn-sm" onclick="loadAll()">🔄 Refresh</button></div>
|
||
</div>
|
||
<div class="tabs">
|
||
<button class="tab active" data-panel="dashboard">📊 Dashboard</button>
|
||
<button class="tab" data-panel="accounts">📧 Accounts</button>
|
||
<button class="tab" data-panel="sendplan">📤 Send Plan</button>
|
||
<button class="tab" data-panel="schedule">📅 Schedule</button>
|
||
<button class="tab" data-panel="controls">⚙️ Controls</button>
|
||
</div>
|
||
|
||
<div id="dashboard" class="panel active">
|
||
<div class="g g5">
|
||
<div class="card kpi"><div class="kpi-val" style="color:var(--orange)" id="kTotal">—</div><div class="kpi-label">Total Accounts</div></div>
|
||
<div class="card kpi"><div class="kpi-val" style="color:var(--green)" id="kWarming">—</div><div class="kpi-label">Warming</div></div>
|
||
<div class="card kpi"><div class="kpi-val" style="color:var(--blue)" id="kGraduated">—</div><div class="kpi-label">Graduated</div></div>
|
||
<div class="card kpi"><div class="kpi-val" style="color:var(--purple)" id="kPending">—</div><div class="kpi-label">Pending</div></div>
|
||
<div class="card kpi"><div class="kpi-val" style="color:var(--red)" id="kPaused">—</div><div class="kpi-label">Paused</div></div>
|
||
</div>
|
||
<div class="g g2">
|
||
<div class="card">
|
||
<div class="card-title">🔥 Today's Volume</div>
|
||
<div class="g g3" style="margin-bottom:0">
|
||
<div class="kpi"><div class="kpi-val" style="color:var(--cyan);font-size:22px" id="kSentToday">—</div><div class="kpi-label">Sent</div></div>
|
||
<div class="kpi"><div class="kpi-val" style="color:var(--orange);font-size:22px" id="kCapacity">—</div><div class="kpi-label">Capacity</div></div>
|
||
<div class="kpi"><div class="kpi-val" style="color:var(--green);font-size:22px" id="kUtilPct">—</div><div class="kpi-label">Utilization</div></div>
|
||
</div>
|
||
<div class="progress-row" style="margin-top:10px"><div class="bar" style="height:10px;border-radius:5px"><div class="bar-fill" id="utilBar" style="width:0%;background:linear-gradient(90deg,var(--orange),var(--green));border-radius:5px"></div></div></div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-title">📊 Volume Last 14 Days</div>
|
||
<div class="chart-container"><div class="chart-bar-row" id="volumeChart"></div><div class="chart-labels" id="volumeLabels"></div></div>
|
||
</div>
|
||
</div>
|
||
<div class="g g32">
|
||
<div class="card"><div class="card-title">📧 Provider Distribution</div><div id="providerDist"><div class="empty">Loading...</div></div></div>
|
||
<div class="card"><div class="card-title">🎯 Day Distribution</div><div id="dayDist"><div class="empty">Loading...</div></div></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="accounts" class="panel">
|
||
<div class="filter-row">
|
||
<select id="fStatus" onchange="loadAccounts()"><option value="">All Status</option><option value="warming">🔥 Warming</option><option value="graduated">🎓 Graduated</option><option value="pending">⏳ Pending</option><option value="paused">⏸️ Paused</option></select>
|
||
<select id="fProvider" onchange="loadAccounts()"><option value="">All Providers</option></select>
|
||
<input type="text" id="fSearch" placeholder="Search email..." oninput="filterTable()">
|
||
<span style="margin-left:auto;font-size:11px;color:var(--text-dim)" id="accCount">—</span>
|
||
</div>
|
||
<div class="card" style="padding:0;overflow:hidden"><div style="overflow-x:auto">
|
||
<table><thead><tr><th>Email</th><th>Provider</th><th>Day</th><th>Limit</th><th>Sent</th><th>Progress</th><th>Status</th><th>Actions</th></tr></thead>
|
||
<tbody id="accBody"><tr><td colspan="8" class="empty">Loading...</td></tr></tbody></table>
|
||
</div></div>
|
||
</div>
|
||
|
||
<div id="sendplan" class="panel">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
|
||
<div class="card-title" style="margin:0">📤 Daily Send Plan</div>
|
||
<div style="display:flex;gap:8px">
|
||
<button class="btn btn-orange btn-sm" onclick="genPlan()">📋 Generate Plan</button>
|
||
<button class="btn btn-green btn-sm" onclick="execWarmup()">🚀 Execute Warmup</button>
|
||
</div>
|
||
</div>
|
||
<div class="g g3" id="planKpis" style="display:none">
|
||
<div class="card kpi"><div class="kpi-val" style="color:var(--cyan);font-size:22px" id="planAccounts">—</div><div class="kpi-label">Accounts in Plan</div></div>
|
||
<div class="card kpi"><div class="kpi-val" style="color:var(--orange);font-size:22px" id="planEmails">—</div><div class="kpi-label">Emails to Send</div></div>
|
||
<div class="card kpi"><div class="kpi-val" style="color:var(--green);font-size:22px" id="planProviders">—</div><div class="kpi-label">Providers</div></div>
|
||
</div>
|
||
<div class="card" style="padding:0;overflow:hidden"><div style="overflow-x:auto">
|
||
<table><thead><tr><th>Email</th><th>Provider</th><th>Day</th><th>To Send</th><th>Already Sent</th><th>Remaining</th></tr></thead>
|
||
<tbody id="planBody"><tr><td colspan="6" class="empty">Click "Generate Plan" to create today's send plan</td></tr></tbody></table>
|
||
</div></div>
|
||
</div>
|
||
|
||
<div id="schedule" class="panel">
|
||
<div class="card" style="margin-bottom:14px">
|
||
<div class="card-title">📅 Warmup Schedule Progression</div>
|
||
<p style="font-size:12px;color:var(--text-dim);margin-bottom:14px">Base limit multiplier per warmup day. Accounts graduate at day 45+ when max limit reached.</p>
|
||
<div class="schedule-grid">
|
||
<div class="schedule-item"><div class="schedule-day">D0</div><div class="schedule-mult">×1</div></div>
|
||
<div class="schedule-item"><div class="schedule-day">D1</div><div class="schedule-mult">×1.5</div></div>
|
||
<div class="schedule-item"><div class="schedule-day">D2</div><div class="schedule-mult">×2</div></div>
|
||
<div class="schedule-item"><div class="schedule-day">D3</div><div class="schedule-mult">×3</div></div>
|
||
<div class="schedule-item"><div class="schedule-day">D4</div><div class="schedule-mult">×4</div></div>
|
||
<div class="schedule-item"><div class="schedule-day">D5</div><div class="schedule-mult">×5</div></div>
|
||
<div class="schedule-item"><div class="schedule-day">D7</div><div class="schedule-mult">×7</div></div>
|
||
<div class="schedule-item"><div class="schedule-day">D10</div><div class="schedule-mult">×10</div></div>
|
||
<div class="schedule-item"><div class="schedule-day">D14</div><div class="schedule-mult">×15</div></div>
|
||
<div class="schedule-item"><div class="schedule-day">D21</div><div class="schedule-mult">×20</div></div>
|
||
<div class="schedule-item"><div class="schedule-day">D28</div><div class="schedule-mult">×30</div></div>
|
||
<div class="schedule-item"><div class="schedule-day">D35</div><div class="schedule-mult">×40</div></div>
|
||
<div class="schedule-item"><div class="schedule-day">D42</div><div class="schedule-mult">×60</div></div>
|
||
<div class="schedule-item"><div class="schedule-day">D50</div><div class="schedule-mult">×80</div></div>
|
||
<div class="schedule-item"><div class="schedule-day">D60</div><div class="schedule-mult">×100</div></div>
|
||
</div>
|
||
</div>
|
||
<div class="g g2">
|
||
<div class="card"><div class="card-title">📧 Provider Base Limits</div>
|
||
<table><thead><tr><th>Provider</th><th>Base/Day</th><th>Max Limit</th></tr></thead><tbody>
|
||
<tr><td>Office 365</td><td style="font-family:var(--mono)">5</td><td style="font-family:var(--mono)">500</td></tr>
|
||
<tr><td>Gmail</td><td style="font-family:var(--mono)">3</td><td style="font-family:var(--mono)">500</td></tr>
|
||
<tr><td>Amazon SES</td><td style="font-family:var(--mono)">10</td><td style="font-family:var(--mono)">10,000</td></tr>
|
||
<tr><td>SendGrid</td><td style="font-family:var(--mono)">5</td><td style="font-family:var(--mono)">100</td></tr>
|
||
<tr><td>Mailgun</td><td style="font-family:var(--mono)">10</td><td style="font-family:var(--mono)">300</td></tr>
|
||
<tr><td>Brevo</td><td style="font-family:var(--mono)">10</td><td style="font-family:var(--mono)">300</td></tr>
|
||
<tr><td>SparkPost</td><td style="font-family:var(--mono)">10</td><td style="font-family:var(--mono)">500</td></tr>
|
||
<tr><td>TurboSMTP</td><td style="font-family:var(--mono)">10</td><td style="font-family:var(--mono)">500</td></tr>
|
||
<tr><td>Zoho</td><td style="font-family:var(--mono)">3</td><td style="font-family:var(--mono)">200</td></tr>
|
||
</tbody></table>
|
||
</div>
|
||
<div class="card"><div class="card-title">📈 O365 Progression Example</div>
|
||
<table><thead><tr><th>Day</th><th>×</th><th>Daily Limit</th><th>Cumulative</th></tr></thead><tbody id="exampleProg"></tbody></table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="controls" class="panel">
|
||
<div class="g g2">
|
||
<div class="card">
|
||
<div class="card-title">🚀 Quick Actions</div>
|
||
<div style="display:flex;flex-direction:column;gap:10px">
|
||
<div style="display:flex;align-items:center;gap:12px"><button class="btn btn-orange" onclick="enrollAll()" style="min-width:160px">📥 Enroll All</button><span style="font-size:11px;color:var(--text-dim)">Auto-enroll new send accounts into warmup</span></div>
|
||
<div style="display:flex;align-items:center;gap:12px"><button class="btn btn-blue" onclick="advanceDay()" style="min-width:160px">📅 Advance Day</button><span style="font-size:11px;color:var(--text-dim)">Advance warmup day +1 for all warming accounts</span></div>
|
||
<div style="display:flex;align-items:center;gap:12px"><button class="btn btn-green" onclick="execWarmup()" style="min-width:160px">🔥 Execute Warmup</button><span style="font-size:11px;color:var(--text-dim)">Run warmup sends for today</span></div>
|
||
<div style="display:flex;align-items:center;gap:12px"><button class="btn btn-orange" onclick="genPlan()" style="min-width:160px">📋 Generate Plan</button><span style="font-size:11px;color:var(--text-dim)">Create today's send plan</span></div>
|
||
</div>
|
||
</div>
|
||
<div class="card"><div class="card-title">📝 Action Log</div><div class="log-box" id="actionLog"><div class="log-info">ℹ️ Ready. Use Quick Actions to manage warmup.</div></div></div>
|
||
</div>
|
||
<div class="card" style="margin-top:14px"><div class="card-title">⚠️ Bulk Operations</div>
|
||
<div style="display:flex;gap:10px;flex-wrap:wrap">
|
||
<button class="btn btn-ghost btn-sm" onclick="bulkAction('pause','warming')">⏸️ Pause All Warming</button>
|
||
<button class="btn btn-ghost btn-sm" onclick="bulkAction('resume','paused')">▶️ Resume All Paused</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="toast" id="toast"></div>
|
||
<script>
|
||
const API='/api/warmup-engine.php';let allAccounts=[];
|
||
document.querySelectorAll('.tab').forEach(t=>{t.addEventListener('click',()=>{document.querySelectorAll('.tab').forEach(x=>x.classList.remove('active'));document.querySelectorAll('.panel').forEach(x=>x.classList.remove('active'));t.classList.add('active');document.getElementById(t.dataset.panel).classList.add('active')})});
|
||
function toast(m,type='ok'){const t=document.getElementById('toast');t.className=`toast toast-${type} show`;t.textContent=m;setTimeout(()=>t.classList.remove('show'),3500)}
|
||
function log(m,cls='info'){const b=document.getElementById('actionLog');const ts=new Date().toLocaleTimeString();b.innerHTML+=`<div><span class="log-ts">[${ts}]</span> <span class="log-${cls}">${m}</span></div>`;b.scrollTop=b.scrollHeight}
|
||
async function api(action,extra={}){try{const p=new URLSearchParams({action,...extra});const r=await fetch(`${API}?${p}`);return await r.json()}catch(e){return{status:'error',message:e.message}}}
|
||
|
||
async function loadStatus(){
|
||
const d=await api('status');
|
||
if(!d||d.status==='error'){document.getElementById('hdrStatus').textContent='❌ API Error';return}
|
||
const s=d.stats||d;
|
||
document.getElementById('hdrStatus').textContent=`Updated: ${new Date().toLocaleTimeString()}`;
|
||
const total=(s.warming||0)+(s.graduated||0)+(s.pending||0)+(s.paused||0);
|
||
document.getElementById('kTotal').textContent=total.toLocaleString();
|
||
document.getElementById('kWarming').textContent=(s.warming||0).toLocaleString();
|
||
document.getElementById('kGraduated').textContent=(s.graduated||0).toLocaleString();
|
||
document.getElementById('kPending').textContent=(s.pending||0).toLocaleString();
|
||
document.getElementById('kPaused').textContent=(s.paused||0).toLocaleString();
|
||
const sent=s.sent_today||0,cap=s.total_capacity||s.daily_capacity||0,pct=cap>0?Math.round(sent/cap*100):0;
|
||
document.getElementById('kSentToday').textContent=sent.toLocaleString();
|
||
document.getElementById('kCapacity').textContent=cap.toLocaleString();
|
||
document.getElementById('kUtilPct').textContent=pct+'%';
|
||
document.getElementById('utilBar').style.width=Math.min(pct,100)+'%';
|
||
if(d.providers||s.providers){
|
||
const provs=d.providers||s.providers;
|
||
const entries=Array.isArray(provs)?provs:Object.entries(provs).map(([k,v])=>({provider:k,count:v}));
|
||
const mx=Math.max(...entries.map(p=>p.count||0),1);entries.sort((a,b)=>(b.count||0)-(a.count||0));
|
||
let h='';entries.forEach(p=>{const pc=Math.round((p.count||0)/mx*100);h+=`<div class="progress-row"><span class="progress-label">${p.provider||p.account_type||'?'}</span><div class="bar"><div class="bar-fill" style="width:${pc}%;background:var(--orange)"></div></div><span class="progress-val">${(p.count||0).toLocaleString()}</span></div>`});
|
||
document.getElementById('providerDist').innerHTML=h||'<div class="empty">No data</div>';
|
||
const sel=document.getElementById('fProvider');if(sel.options.length<=1){entries.forEach(p=>{const o=document.createElement('option');o.value=p.provider||p.account_type||'';o.textContent=`${o.value} (${p.count||0})`;sel.appendChild(o)})}
|
||
}
|
||
if(d.day_distribution||s.day_distribution){
|
||
const days=d.day_distribution||s.day_distribution;
|
||
const entries=Array.isArray(days)?days:Object.entries(days).map(([k,v])=>({day_range:k,count:v}));
|
||
const mx=Math.max(...entries.map(x=>x.count||0),1);
|
||
let h='';entries.forEach(dd=>{const pc=Math.round((dd.count||0)/mx*100);h+=`<div class="progress-row"><span class="progress-label">${dd.day_range||dd.range||'D'+dd.day}</span><div class="bar"><div class="bar-fill" style="width:${pc}%;background:var(--cyan)"></div></div><span class="progress-val">${(dd.count||0).toLocaleString()}</span></div>`});
|
||
document.getElementById('dayDist').innerHTML=h||'<div class="empty">No data</div>';
|
||
}
|
||
if(d.daily_volumes||s.daily_volumes){
|
||
const vols=(d.daily_volumes||s.daily_volumes).slice(-14);if(vols.length>0){
|
||
const mx=Math.max(...vols.map(v=>v.sent||v.count||0),1);let bars='',labels='';
|
||
vols.forEach(v=>{const ht=Math.max(((v.sent||v.count||0)/mx)*150,2);const day=(v.date||'').slice(-5);bars+=`<div class="chart-bar" style="height:${ht}px"><div class="tip">${day}: ${(v.sent||v.count||0).toLocaleString()}</div></div>`;labels+=`<span>${day}</span>`});
|
||
document.getElementById('volumeChart').innerHTML=bars;document.getElementById('volumeLabels').innerHTML=labels}}
|
||
}
|
||
|
||
async function loadAccounts(){
|
||
const st=document.getElementById('fStatus').value,pr=document.getElementById('fProvider').value;
|
||
const d=await api('status');if(!d||!d.accounts){document.getElementById('accBody').innerHTML='<tr><td colspan="8" class="empty">No data</td></tr>';return}
|
||
allAccounts=d.accounts||[];let f=allAccounts;if(st)f=f.filter(a=>a.status===st);if(pr)f=f.filter(a=>(a.account_type||a.provider)===pr);
|
||
document.getElementById('accCount').textContent=`${f.length} / ${allAccounts.length}`;renderAccounts(f);
|
||
}
|
||
|
||
function renderAccounts(accs){
|
||
if(!accs||!accs.length){document.getElementById('accBody').innerHTML='<tr><td colspan="8" class="empty">No accounts</td></tr>';return}
|
||
let h='';accs.slice(0,200).forEach(a=>{
|
||
const cls={warming:'b-warm',graduated:'b-grad',pending:'b-pend',paused:'b-pause',active:'b-active'}[a.status]||'b-warm';
|
||
const pct=a.daily_limit>0?Math.round((a.sent_today||0)/a.daily_limit*100):0;
|
||
const bc=pct>=80?'var(--green)':pct>=50?'var(--amber)':'var(--blue)';
|
||
h+=`<tr><td style="font-family:var(--mono);font-size:11px">${a.email||'—'}</td><td>${a.account_type||a.provider||'—'}</td><td style="font-family:var(--mono);text-align:center">${a.current_day||0}</td><td style="font-family:var(--mono);text-align:center">${a.daily_limit||0}</td><td style="font-family:var(--mono);text-align:center">${a.sent_today||0}</td><td><div class="progress-row" style="margin:0"><div class="bar"><div class="bar-fill" style="width:${Math.min(pct,100)}%;background:${bc}"></div></div><span style="font-family:var(--mono);font-size:10px;width:35px;text-align:right">${pct}%</span></div></td><td><span class="badge ${cls}">${a.status||'—'}</span></td><td>${a.status==='warming'?`<button class="btn btn-ghost btn-sm" onclick="singleAction('pause','${a.id||a.email}')">⏸️</button>`:''}${a.status==='paused'?`<button class="btn btn-ghost btn-sm" onclick="singleAction('resume','${a.id||a.email}')">▶️</button>`:''}</td></tr>`});
|
||
document.getElementById('accBody').innerHTML=h;
|
||
}
|
||
|
||
function filterTable(){const q=document.getElementById('fSearch').value.toLowerCase(),st=document.getElementById('fStatus').value,pr=document.getElementById('fProvider').value;let f=allAccounts;if(st)f=f.filter(a=>a.status===st);if(pr)f=f.filter(a=>(a.account_type||a.provider)===pr);if(q)f=f.filter(a=>(a.email||'').toLowerCase().includes(q));document.getElementById('accCount').textContent=`${f.length} / ${allAccounts.length}`;renderAccounts(f)}
|
||
|
||
async function genPlan(){toast('Generating plan...','info');log('Generating send plan...','info');const d=await api('daily_send_plan');if(d.status==='success'||d.plan){const plan=d.plan||d.accounts||[];document.getElementById('planKpis').style.display='grid';document.getElementById('planAccounts').textContent=plan.length;document.getElementById('planEmails').textContent=plan.reduce((s,a)=>s+(a.to_send||a.remaining||0),0).toLocaleString();document.getElementById('planProviders').textContent=new Set(plan.map(a=>a.account_type||a.provider)).size;let h='';plan.slice(0,300).forEach(a=>{h+=`<tr><td style="font-family:var(--mono);font-size:11px">${a.email||'—'}</td><td>${a.account_type||a.provider||'—'}</td><td style="font-family:var(--mono);text-align:center">${a.current_day||0}</td><td style="font-family:var(--mono);text-align:center;color:var(--orange)">${a.to_send||a.daily_limit||0}</td><td style="font-family:var(--mono);text-align:center">${a.sent_today||0}</td><td style="font-family:var(--mono);text-align:center;color:var(--green)">${(a.to_send||a.daily_limit||0)-(a.sent_today||0)}</td></tr>`});document.getElementById('planBody').innerHTML=h||'<tr><td colspan="6" class="empty">No accounts in plan</td></tr>';toast(`Plan: ${plan.length} accounts`,'ok');log(`✅ Plan: ${plan.length} accounts`,'ok')}else{toast('Plan failed','err');log('❌ Plan failed: '+(d.message||'?'),'err')}}
|
||
|
||
async function execWarmup(){if(!confirm('Execute warmup sends for today?'))return;toast('Executing...','info');log('🔥 Executing warmup...','warn');const d=await api('execute_warmup');if(d.status==='success'){toast(`Sent: ${d.sent||0} emails`,'ok');log(`✅ Done: ${d.sent||0} sent, ${d.errors||0} errors`,'ok');loadStatus()}else{toast('Failed','err');log('❌ '+( d.message||'?'),'err')}}
|
||
async function enrollAll(){toast('Enrolling...','info');log('📥 Enrolling...','info');const d=await api('enroll_all');if(d.status==='success'){toast(`Enrolled ${d.enrolled||0} (total: ${d.total_warmup||'?'})`,'ok');log(`✅ Enrolled ${d.enrolled||0}. Total: ${d.total_warmup||'?'}`,'ok');loadAll()}else{toast('Failed','err');log('❌ '+(d.message||'?'),'err')}}
|
||
async function advanceDay(){if(!confirm('Advance day for all warming accounts?'))return;toast('Advancing...','info');log('📅 Advancing...','info');const d=await api('advance_day');if(d.status==='success'){toast(`Advanced ${d.advanced||0}, graduated ${d.graduated||0}`,'ok');log(`✅ Advanced ${d.advanced||0}, graduated ${d.graduated||0}`,'ok');loadAll()}else{toast('Failed','err');log('❌ '+(d.message||'?'),'err')}}
|
||
async function singleAction(action,id){const d=await api(action,{id});toast(`${action}: ${d.status||'done'}`,d.status==='success'?'ok':'err');loadAccounts()}
|
||
async function bulkAction(action,target){if(!confirm(`${action} all ${target}?`))return;toast(`Bulk ${action}...`,'info');log(`🔄 Bulk ${action}...`,'info');const d=await api(action,{bulk:target});toast(`Done: ${d.affected||d.count||'?'}`,'ok');log(`✅ Bulk ${action}: ${d.affected||d.count||'?'}`,'ok');loadAll()}
|
||
|
||
function renderExample(){const sched=[[0,1],[1,1.5],[2,2],[3,3],[4,4],[5,5],[7,7],[10,10],[14,15],[21,20],[28,30],[35,40],[42,60],[50,80],[60,100]];let h='',c=0;sched.forEach(([day,mult])=>{const lim=Math.min(Math.round(5*mult),500);c+=lim;h+=`<tr><td style="font-family:var(--mono);color:var(--orange)">D${day}</td><td style="font-family:var(--mono)">×${mult}</td><td style="font-family:var(--mono);color:var(--cyan)">${lim}/d</td><td style="font-family:var(--mono);color:var(--text-dim)">${c.toLocaleString()}</td></tr>`});document.getElementById('exampleProg').innerHTML=h}
|
||
|
||
async function loadAll(){loadStatus();loadAccounts();renderExample()}
|
||
loadAll();setInterval(loadStatus,30000);
|
||
</script><script src="arsenal-common.js?v1770778169">
|
||
</body></html>
|
||
</script>
|