Files
wevads-platform/public/warming-engine.html
2026-02-26 04:53:11 +01:00

301 lines
28 KiB
HTML
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>