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

591 lines
29 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pipeline Command Center — WEVADS</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
:root {
--bg: #06080f; --surface: #0c1017; --surface2: #121820; --surface3: #1a222d;
--border: #1e2a38; --border2: #2a3a4d; --text: #e8edf5; --text2: #8b9ab5; --text3: #5a6a80;
--green: #00d67e; --red: #ff4d6a; --blue: #3b82f6; --amber: #f59e0b; --purple: #a855f7;
--cyan: #06b6d4; --pink: #ec4899;
}
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:'Plus Jakarta Sans',sans-serif; background:var(--bg); color:var(--text); min-height:100vh; }
::-webkit-scrollbar { width:6px; } ::-webkit-scrollbar-track { background:var(--surface); } ::-webkit-scrollbar-thumb { background:var(--border2); border-radius:3px; }
/* HEADER */
.header { background:var(--surface); border-bottom:1px solid var(--border); padding:14px 28px; display:flex; align-items:center; gap:16px; position:sticky; top:0; z-index:100; }
.header h1 { font-size:18px; font-weight:800; letter-spacing:-0.5px; }
.header h1 span { color:var(--cyan); }
.header .pill { padding:3px 10px; border-radius:20px; font-size:11px; font-weight:600; }
.pill-live { background:rgba(0,214,126,.12); color:var(--green); border:1px solid rgba(0,214,126,.25); }
.pill-count { background:var(--surface3); color:var(--text2); }
/* TABS */
.tabs { display:flex; gap:2px; padding:12px 28px; background:var(--surface); border-bottom:1px solid var(--border); overflow-x:auto; }
.tab { padding:8px 18px; border-radius:8px; font-size:13px; font-weight:600; cursor:pointer; color:var(--text3); transition:all .15s; white-space:nowrap; border:1px solid transparent; }
.tab:hover { color:var(--text2); background:var(--surface2); }
.tab.active { color:var(--cyan); background:rgba(6,182,212,.08); border-color:rgba(6,182,212,.2); }
/* CONTENT */
.content { padding:20px 28px; }
.panel { display:none; } .panel.active { display:block; }
/* STATS GRID */
.stats { display:grid; grid-template-columns:repeat(auto-fill,minmax(170px,1fr)); gap:12px; margin-bottom:24px; }
.stat { background:var(--surface); border:1px solid var(--border); border-radius:10px; padding:16px; }
.stat .label { font-size:11px; color:var(--text3); text-transform:uppercase; letter-spacing:.5px; font-weight:600; margin-bottom:6px; }
.stat .val { font-family:'JetBrains Mono',monospace; font-size:24px; font-weight:700; }
.stat .val.green { color:var(--green); } .stat .val.blue { color:var(--blue); } .stat .val.amber { color:var(--amber); } .stat .val.purple { color:var(--purple); } .stat .val.cyan { color:var(--cyan); }
/* TABLE */
.tbl-wrap { background:var(--surface); border:1px solid var(--border); border-radius:10px; overflow:hidden; }
.tbl-header { padding:14px 18px; display:flex; align-items:center; gap:12px; border-bottom:1px solid var(--border); }
.tbl-header h3 { font-size:14px; font-weight:700; flex:1; }
.btn { padding:7px 16px; border-radius:7px; font-size:12px; font-weight:600; cursor:pointer; border:none; transition:all .15s; font-family:inherit; }
.btn-primary { background:var(--cyan); color:#000; } .btn-primary:hover { filter:brightness(1.15); }
.btn-green { background:var(--green); color:#000; } .btn-green:hover { filter:brightness(1.15); }
.btn-ghost { background:var(--surface3); color:var(--text2); border:1px solid var(--border); } .btn-ghost:hover { border-color:var(--border2); color:var(--text); }
.btn-sm { padding:4px 10px; font-size:11px; }
.btn-danger { background:rgba(255,77,106,.12); color:var(--red); border:1px solid rgba(255,77,106,.2); } .btn-danger:hover { background:rgba(255,77,106,.2); }
table { width:100%; border-collapse:collapse; font-size:13px; }
th { text-align:left; padding:10px 14px; color:var(--text3); font-weight:600; font-size:11px; text-transform:uppercase; letter-spacing:.5px; border-bottom:1px solid var(--border); background:var(--surface2); }
td { padding:10px 14px; border-bottom:1px solid rgba(30,42,56,.5); }
tr:hover td { background:rgba(6,182,212,.03); }
.badge { display:inline-block; padding:2px 8px; border-radius:4px; font-size:11px; font-weight:600; }
.badge-green { background:rgba(0,214,126,.12); color:var(--green); } .badge-red { background:rgba(255,77,106,.12); color:var(--red); }
.badge-blue { background:rgba(59,130,246,.12); color:var(--blue); } .badge-amber { background:rgba(245,158,11,.12); color:var(--amber); }
.badge-purple { background:rgba(168,85,247,.12); color:var(--purple); }
.mono { font-family:'JetBrains Mono',monospace; font-size:12px; }
.img-thumb { width:40px; height:40px; border-radius:6px; object-fit:cover; border:1px solid var(--border); background:var(--surface3); }
/* MODAL */
.modal-overlay { position:fixed; inset:0; background:rgba(0,0,0,.65); z-index:200; display:none; align-items:center; justify-content:center; backdrop-filter:blur(4px); }
.modal-overlay.show { display:flex; }
.modal { background:var(--surface); border:1px solid var(--border2); border-radius:14px; width:90%; max-width:620px; max-height:85vh; overflow-y:auto; }
.modal-head { padding:18px 22px; border-bottom:1px solid var(--border); display:flex; align-items:center; }
.modal-head h3 { flex:1; font-size:16px; font-weight:700; }
.modal-head .close { width:30px; height:30px; border-radius:8px; background:var(--surface3); border:none; color:var(--text2); cursor:pointer; font-size:16px; display:flex; align-items:center; justify-content:center; }
.modal-body { padding:22px; }
.field { margin-bottom:16px; }
.field label { display:block; font-size:12px; font-weight:600; color:var(--text2); margin-bottom:6px; }
.field input, .field select, .field textarea { width:100%; padding:10px 14px; background:var(--surface2); border:1px solid var(--border); border-radius:8px; color:var(--text); font-size:13px; font-family:inherit; outline:none; }
.field input:focus, .field select:focus { border-color:var(--cyan); }
.field textarea { min-height:80px; resize:vertical; }
.field-row { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
.modal-foot { padding:14px 22px; border-top:1px solid var(--border); display:flex; gap:10px; justify-content:flex-end; }
/* BRAND POOL */
.brand-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(150px,1fr)); gap:10px; margin:16px 0; }
.brand-card { background:var(--surface2); border:1px solid var(--border); border-radius:8px; padding:14px; text-align:center; }
.brand-card .name { font-weight:700; font-size:15px; margin-bottom:4px; }
.brand-card .count { font-family:'JetBrains Mono',monospace; font-size:22px; font-weight:700; color:var(--cyan); }
.brand-card .sub { font-size:11px; color:var(--text3); }
/* TOAST */
.toast { position:fixed; bottom:24px; right:24px; padding:12px 20px; border-radius:10px; font-size:13px; font-weight:600; z-index:300; transform:translateY(80px); opacity:0; transition:all .3s; }
.toast.show { transform:translateY(0); opacity:1; }
.toast-ok { background:var(--green); color:#000; } .toast-err { background:var(--red); color:#fff; }
</style>
</head>
<body>
<div class="header">
<h1>⚡ Pipeline <span>Command Center</span></h1>
<span class="pill pill-live">● LIVE</span>
<span class="pill pill-count" id="hdr-sends">— sends</span>
<span class="pill pill-count" id="hdr-accounts">— accounts</span>
</div>
<div class="tabs" id="tabs">
<div class="tab active" data-tab="dashboard">📊 Dashboard</div>
<div class="tab" data-tab="creatives">🎨 Creatives</div>
<div class="tab" data-tab="offers">💰 Offers</div>
<div class="tab" data-tab="accounts">📧 O365 Accounts</div>
<div class="tab" data-tab="brain">🧠 Brain Config</div>
<div class="tab" data-tab="isp">🌐 ISP Routing</div>
<div class="tab" data-tab="methods">⚙️ Send Methods</div>
<div class="tab" data-tab="log">📋 Send Log</div>
<div class="tab" data-tab="test">🚀 Test Send</div>
</div>
<div class="content">
<!-- ═══════ DASHBOARD ═══════ -->
<div class="panel active" id="p-dashboard">
<div class="stats" id="stats-grid"></div>
<div class="tbl-wrap" style="margin-bottom:20px">
<div class="tbl-header"><h3>🏷️ Brand Account Pool</h3></div>
<div class="brand-grid" id="brand-pool" style="padding:16px"></div>
</div>
</div>
<!-- ═══════ CREATIVES ═══════ -->
<div class="panel" id="p-creatives">
<div class="tbl-wrap">
<div class="tbl-header">
<h3>🎨 Active Creatives</h3>
<button class="btn btn-primary" onclick="openCreativeModal()">+ New Creative</button>
</div>
<table><thead><tr><th>ID</th><th>Img</th><th>From</th><th>Subject</th><th>Offer</th><th>Payout</th><th>Status</th><th>Actions</th></tr></thead>
<tbody id="tbl-creatives"></tbody></table>
</div>
</div>
<!-- ═══════ OFFERS ═══════ -->
<div class="panel" id="p-offers">
<div class="tbl-wrap">
<div class="tbl-header">
<h3>💰 Sponsor Offers</h3>
<button class="btn btn-primary" onclick="openOfferModal()">+ New Offer</button>
</div>
<table><thead><tr><th>ID</th><th>Name</th><th>Payout</th><th>Country</th><th>Vertical</th><th>CX3 URL</th><th>Status</th><th>Actions</th></tr></thead>
<tbody id="tbl-offers"></tbody></table>
</div>
</div>
<!-- ═══════ O365 ACCOUNTS ═══════ -->
<div class="panel" id="p-accounts">
<div class="tbl-wrap">
<div class="tbl-header"><h3>📧 O365 Graph Accounts (can send)</h3></div>
<table><thead><tr><th>ID</th><th>Email</th><th>Brand</th><th>Status</th><th>Tenant</th><th>Actions</th></tr></thead>
<tbody id="tbl-accounts"></tbody></table>
</div>
</div>
<!-- ═══════ BRAIN CONFIG ═══════ -->
<div class="panel" id="p-brain">
<div class="tbl-wrap">
<div class="tbl-header"><h3>🧠 Brain Configs — ISP × Method Performance</h3></div>
<table><thead><tr><th>ISP</th><th>Method</th><th>Inbox %</th><th>Winner</th><th>Tests</th><th>Domain</th></tr></thead>
<tbody id="tbl-brain"></tbody></table>
</div>
</div>
<!-- ═══════ ISP ROUTING ═══════ -->
<div class="panel" id="p-isp">
<div class="tbl-wrap">
<div class="tbl-header"><h3>🌐 ISP Default Routing</h3></div>
<table><thead><tr><th>ISP</th><th>Default Method</th><th>Fallback</th><th>Priority</th></tr></thead>
<tbody id="tbl-isp"></tbody></table>
</div>
</div>
<!-- ═══════ SEND METHODS ═══════ -->
<div class="panel" id="p-methods">
<div class="tbl-wrap">
<div class="tbl-header"><h3>⚙️ Active Send Methods</h3></div>
<table><thead><tr><th>ID</th><th>Method</th><th>Type</th><th>Priority</th><th>Active</th></tr></thead>
<tbody id="tbl-methods"></tbody></table>
</div>
</div>
<!-- ═══════ SEND LOG ═══════ -->
<div class="panel" id="p-log">
<div class="tbl-wrap">
<div class="tbl-header">
<h3>📋 Send Log (Today)</h3>
<button class="btn btn-ghost" onclick="loadLog()">↻ Refresh</button>
</div>
<table><thead><tr><th>ID</th><th>To</th><th>Subject</th><th>Method</th><th>Status</th><th>Time</th></tr></thead>
<tbody id="tbl-log"></tbody></table>
</div>
</div>
<!-- ═══════ TEST SEND ═══════ -->
<div class="panel" id="p-test">
<div class="tbl-wrap" style="max-width:600px">
<div class="tbl-header"><h3>🚀 Test Send</h3></div>
<div style="padding:22px">
<div class="field"><label>Creative</label><select id="test-creative"></select></div>
<div class="field"><label>Recipient</label><input id="test-to" value="yacineutt@gmail.com"></div>
<button class="btn btn-green" onclick="testSend()" id="btn-test">Send Test Email</button>
<div id="test-result" style="margin-top:16px; font-family:'JetBrains Mono',monospace; font-size:13px;"></div>
</div>
</div>
</div>
</div><!-- /content -->
<!-- ═══════ CREATIVE MODAL ═══════ -->
<div class="modal-overlay" id="modal-creative">
<div class="modal">
<div class="modal-head"><h3 id="modal-c-title">New Creative</h3><button class="close" onclick="closeModal('modal-creative')"></button></div>
<div class="modal-body">
<input type="hidden" id="c-id">
<div class="field-row">
<div class="field"><label>From Name</label><input id="c-from" placeholder="Wellnee"></div>
<div class="field"><label>Offer</label><select id="c-offer"></select></div>
</div>
<div class="field"><label>Subject Line</label><input id="c-subject" placeholder="Neu: Schmerzfrei in 15 Minuten"></div>
<div class="field"><label>Image URL</label><input id="c-image" placeholder="https://track.wevup.app/media/..."></div>
<div class="field-row">
<div class="field"><label>Unsub Text</label><input id="c-unsub" value="Abmelden"></div>
<div class="field"><label>Status</label><select id="c-status"><option value="active">Active</option><option value="disabled">Disabled</option></select></div>
</div>
<div style="margin-top:12px"><label style="font-size:12px;font-weight:600;color:var(--text2)">Image Preview</label>
<div style="margin-top:8px;background:var(--surface3);border-radius:8px;padding:12px;text-align:center;min-height:60px" id="c-preview">No image</div>
</div>
</div>
<div class="modal-foot">
<button class="btn btn-ghost" onclick="closeModal('modal-creative')">Cancel</button>
<button class="btn btn-primary" onclick="saveCreative()">Save</button>
</div>
</div>
</div>
<!-- ═══════ OFFER MODAL ═══════ -->
<div class="modal-overlay" id="modal-offer">
<div class="modal">
<div class="modal-head"><h3 id="modal-o-title">New Offer</h3><button class="close" onclick="closeModal('modal-offer')"></button></div>
<div class="modal-body">
<input type="hidden" id="o-id">
<div class="field"><label>Name</label><input id="o-name" placeholder="Wellnee DE"></div>
<div class="field-row">
<div class="field"><label>Payout ($)</label><input id="o-payout" type="number" step="0.01"></div>
<div class="field"><label>Country</label><input id="o-country" placeholder="DE"></div>
</div>
<div class="field"><label>CX3 Tracking URL</label><input id="o-tracking" placeholder="https://e36lbat.com/?offer_id=...&aff_id=10805"></div>
<div class="field"><label>Preview URL</label><input id="o-preview" placeholder="https://..."></div>
<div class="field-row">
<div class="field"><label>Vertical</label><input id="o-vertical" placeholder="Health"></div>
<div class="field"><label>Status</label><select id="o-status"><option value="active">Active</option><option value="paused">Paused</option></select></div>
</div>
</div>
<div class="modal-foot">
<button class="btn btn-ghost" onclick="closeModal('modal-offer')">Cancel</button>
<button class="btn btn-primary" onclick="saveOffer()">Save</button>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
const API = 'pipeline-admin-api.php';
// ═══════ TABS ═══════
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('p-'+t.dataset.tab).classList.add('active');
loaders[t.dataset.tab]?.();
}));
// ═══════ API ═══════
async function api(action, body=null) {
const opts = body ? {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body)} : {};
const r = await fetch(`${API}?action=${action}`, opts);
return r.json();
}
function toast(msg, ok=true) {
const t = document.getElementById('toast');
t.textContent = msg; t.className = 'toast show ' + (ok?'toast-ok':'toast-err');
setTimeout(()=> t.classList.remove('show'), 3000);
}
// ═══════ DASHBOARD ═══════
async function loadDashboard() {
const s = await api('stats');
document.getElementById('hdr-sends').textContent = s.sends_today + ' sends today';
document.getElementById('hdr-accounts').textContent = s.o365_graph_send + ' senders';
const grid = document.getElementById('stats-grid');
grid.innerHTML = [
['Creatives', s.creatives_active, 'cyan'],
['CX3 Offers', s.offers, 'green'],
['O365 Senders', s.o365_graph_send, 'purple'],
['O365 Graph OK', s.o365_graph_ok, 'blue'],
['O365 Total', s.o365_total, 'amber'],
['Send Methods', s.send_methods, 'cyan'],
['Brain Configs', s.brain_configs, 'purple'],
['FreeDNS Domains', s.domains_verified, 'green'],
['Sends Today', s.sends_today, 'amber'],
['Contacts', s.contacts || '—', 'blue'],
].map(([l,v,c])=>`<div class="stat"><div class="label">${l}</div><div class="val ${c}">${v}</div></div>`).join('');
// === CONTACTS ISP BREAKDOWN ===
const ispEl = document.getElementById('contacts-breakdown');
if (ispEl && s.contacts_isps) {
const isps = s.contacts_isps;
const total = s.contacts_raw || 0;
const colors = {gmail:'#ea4335',hotmail:'#0078d4',spectrum:'#00bcd4',toline:'#ff9800',gmx:'#4caf50'};
ispEl.innerHTML = `
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px">
<span style="font-size:14px;font-weight:700;color:#22d3ee">📊 Contacts Database</span>
<span style="font-size:20px;font-weight:800;color:#f59e0b">${Number(total).toLocaleString()}</span>
<span style="font-size:11px;color:#64748b">(${isps.length} ISPs, ${isps.reduce((a,b)=>a+b.tables,0)} tables)</span>
</div>
<div style="display:flex;gap:2px;height:8px;border-radius:4px;overflow:hidden;margin-bottom:12px">
${isps.map(i=>`<div style="flex:${i.contacts};background:${colors[i.isp]||'#64748b'}" title="${i.isp}: ${Number(i.contacts).toLocaleString()}"></div>`).join('')}
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:8px">
${isps.sort((a,b)=>b.contacts-a.contacts).map(i=>`
<div style="background:rgba(15,23,42,.6);border:1px solid ${colors[i.isp]||'#334155'}40;border-radius:8px;padding:10px 12px;display:flex;align-items:center;gap:10px">
<div style="width:8px;height:32px;border-radius:4px;background:${colors[i.isp]||'#64748b'}"></div>
<div style="flex:1">
<div style="font-weight:700;font-size:13px;text-transform:uppercase;color:${colors[i.isp]||'#94a3b8'}">${i.isp}</div>
<div style="font-size:16px;font-weight:800;color:#e2e8f0">${Number(i.contacts).toLocaleString()}</div>
</div>
<div style="text-align:right">
<div style="font-size:10px;color:#64748b">${i.tables} tables</div>
<div style="font-size:11px;font-weight:600;color:#94a3b8">${(i.contacts/total*100).toFixed(1)}%</div>
</div>
</div>
`).join('')}
</div>`;
}
const pool = document.getElementById('brand-pool');
pool.innerHTML = (s.brand_pool||[]).map(b=>
`<div class="brand-card"><div class="name">${b.brand}</div><div class="count">${b.cnt}</div><div class="sub">accounts</div></div>`
).join('');
}
// ═══════ CREATIVES ═══════
let allOffers = [];
async function loadCreatives() {
const [creatives, offers] = await Promise.all([api('creatives_list'), api('offers_list')]);
allOffers = offers;
const tbody = document.getElementById('tbl-creatives');
tbody.innerHTML = creatives.filter(c=>c.status==='active').map(c => `<tr>
<td class="mono">${c.id}</td>
<td>${c.s3_image_url ? `<img class="img-thumb" src="${c.s3_image_url}" onerror="this.style.display='none'">` : '—'}</td>
<td><strong>${c.from_name||'—'}</strong></td>
<td>${c.subject_line||'—'}</td>
<td>${c.offer_name||'—'}</td>
<td class="mono">${c.payout ? '$'+c.payout : '—'}</td>
<td><span class="badge ${c.status==='active'?'badge-green':'badge-red'}">${c.status}</span></td>
<td>
<button class="btn btn-sm btn-ghost" onclick="editCreative(${c.id})">Edit</button>
<button class="btn btn-sm btn-danger" onclick="deleteCreative(${c.id})">×</button>
</td>
</tr>`).join('');
// Populate test dropdown
const sel = document.getElementById('test-creative');
sel.innerHTML = creatives.filter(c=>c.status==='active').map(c=>
`<option value="${c.id}">#${c.id} ${c.from_name}${c.subject_line}</option>`
).join('');
}
let creativesCache = [];
async function editCreative(id) {
if(!allOffers.length) allOffers = await api('offers_list');
const creatives = await api('creatives_list');
creativesCache = creatives;
const c = creatives.find(x=>x.id==id);
if(!c) return;
document.getElementById('c-id').value = c.id;
document.getElementById('c-from').value = c.from_name||'';
document.getElementById('c-subject').value = c.subject_line||'';
document.getElementById('c-image').value = c.s3_image_url||'';
document.getElementById('c-status').value = c.status;
document.getElementById('c-unsub').value = 'Abmelden';
document.getElementById('modal-c-title').textContent = 'Edit Creative #'+c.id;
populateOfferSelect('c-offer', c.offer_id);
updatePreview();
document.getElementById('modal-creative').classList.add('show');
}
function openCreativeModal() {
document.getElementById('c-id').value = '';
document.getElementById('c-from').value = '';
document.getElementById('c-subject').value = '';
document.getElementById('c-image').value = '';
document.getElementById('c-status').value = 'active';
document.getElementById('modal-c-title').textContent = 'New Creative';
populateOfferSelect('c-offer');
document.getElementById('c-preview').innerHTML = 'No image';
document.getElementById('modal-creative').classList.add('show');
}
function populateOfferSelect(selId, selected='') {
const sel = document.getElementById(selId);
sel.innerHTML = allOffers.map(o=>`<option value="${o.id}" ${o.id==selected?'selected':''}>${o.name} ($${o.payout})</option>`).join('');
}
document.getElementById('c-image')?.addEventListener('input', updatePreview);
function updatePreview() {
const url = document.getElementById('c-image').value;
document.getElementById('c-preview').innerHTML = url ? `<img src="${url}" style="max-width:100%;max-height:200px;border-radius:6px" onerror="this.parentNode.innerHTML='Image not found'">` : 'No image';
}
async function saveCreative() {
const data = {
id: document.getElementById('c-id').value || 0,
from_name: document.getElementById('c-from').value,
subject_line: document.getElementById('c-subject').value,
image_url: document.getElementById('c-image').value,
offer_id: document.getElementById('c-offer').value,
unsub_text: document.getElementById('c-unsub').value,
status: document.getElementById('c-status').value,
};
const r = await api('creative_save', data);
closeModal('modal-creative');
toast(r.ok ? 'Creative saved!' : 'Error', r.ok);
loadCreatives();
}
async function deleteCreative(id) {
if(!confirm('Disable creative #'+id+'?')) return;
await api('creative_delete&id='+id);
toast('Creative disabled'); loadCreatives();
}
// ═══════ OFFERS ═══════
async function loadOffers() {
const offers = await api('offers_list');
allOffers = offers;
document.getElementById('tbl-offers').innerHTML = offers.map(o=>`<tr>
<td class="mono">${o.id}</td>
<td><strong>${o.name}</strong></td>
<td class="mono" style="color:var(--green)">$${o.payout}</td>
<td>${o.country_code}</td>
<td>${o.vertical}</td>
<td class="mono" style="max-width:200px;overflow:hidden;text-overflow:ellipsis">${o.tracking_url||'—'}</td>
<td><span class="badge ${o.status==='active'?'badge-green':'badge-amber'}">${o.status}</span></td>
<td><button class="btn btn-sm btn-ghost" onclick="editOffer(${o.id})">Edit</button></td>
</tr>`).join('');
}
function openOfferModal() {
['o-id','o-name','o-payout','o-country','o-tracking','o-preview','o-vertical'].forEach(id=>document.getElementById(id).value='');
document.getElementById('o-status').value='active';
document.getElementById('modal-o-title').textContent='New Offer';
document.getElementById('modal-offer').classList.add('show');
}
async function editOffer(id) {
const offers = await api('offers_list');
const o = offers.find(x=>x.id==id); if(!o) return;
document.getElementById('o-id').value=o.id;
document.getElementById('o-name').value=o.name||'';
document.getElementById('o-payout').value=o.payout;
document.getElementById('o-country').value=o.country_code||'';
document.getElementById('o-tracking').value=o.tracking_url||'';
document.getElementById('o-preview').value=o.preview_url||'';
document.getElementById('o-vertical').value=o.vertical||'';
document.getElementById('o-status').value=o.status;
document.getElementById('modal-o-title').textContent='Edit Offer #'+o.id;
document.getElementById('modal-offer').classList.add('show');
}
async function saveOffer() {
const data = {
id:document.getElementById('o-id').value||0,
name:document.getElementById('o-name').value,
payout:document.getElementById('o-payout').value,
country_code:document.getElementById('o-country').value,
tracking_url:document.getElementById('o-tracking').value,
preview_url:document.getElementById('o-preview').value,
vertical:document.getElementById('o-vertical').value,
status:document.getElementById('o-status').value,
};
const r = await api('offer_save', data);
closeModal('modal-offer');
toast(r.ok?'Offer saved!':'Error',r.ok); loadOffers();
}
// ═══════ ACCOUNTS ═══════
async function loadAccounts() {
const accs = await api('accounts_list');
document.getElementById('tbl-accounts').innerHTML = accs.map(a=>`<tr>
<td class="mono">${a.id}</td>
<td class="mono" style="font-size:11px">${a.admin_email}</td>
<td><span class="badge badge-purple">${a.brand||'—'}</span></td>
<td><span class="badge ${a.password_status==='graph_send'?'badge-green':'badge-blue'}">${a.password_status}</span></td>
<td>${a.tenant_domain||'—'}</td>
<td><select class="btn btn-sm btn-ghost" onchange="rebrand(${a.id},this.value)" style="padding:3px 8px;font-size:11px">
${['Wellnee','Lulutox','Akusoli','GlucoTrust','Service'].map(b=>`<option ${a.brand===b?'selected':''}>${b}</option>`).join('')}
</select></td>
</tr>`).join('');
}
async function rebrand(id,brand) {
await api('account_rebrand',{id,brand});
toast('Rebranded to '+brand);
}
// ═══════ BRAIN ═══════
async function loadBrain() {
const rows = await api('brain_list');
document.getElementById('tbl-brain').innerHTML = rows.map(r=>`<tr>
<td><strong>${r.isp}</strong></td>
<td class="mono">${r.send_method}</td>
<td class="mono" style="color:${parseFloat(r.inbox_rate)>=80?'var(--green)':parseFloat(r.inbox_rate)>=50?'var(--amber)':'var(--red)'}">${r.inbox_rate}%</td>
<td>${r.is_winner==='t'?'⭐ Winner':'—'}</td>
<td class="mono">${r.tests_count||0}</td>
<td class="mono" style="font-size:11px">${r.domain_used||'—'}</td>
</tr>`).join('');
}
// ═══════ ISP ═══════
async function loadIsp() {
const rows = await api('isp_methods_list');
document.getElementById('tbl-isp').innerHTML = rows.map(r=>`<tr>
<td><strong>${r.isp}</strong></td>
<td class="mono">${r.method||'—'}</td>
<td class="mono">${r.fallback_method||'—'}</td>
<td class="mono">${r.priority||'—'}</td>
</tr>`).join('');
}
// ═══════ METHODS ═══════
async function loadMethods() {
const rows = await api('methods_list');
document.getElementById('tbl-methods').innerHTML = rows.map(r=>`<tr>
<td class="mono">${r.id}</td>
<td><strong>${r.method_name}</strong></td>
<td class="mono">${r.method_type||'—'}</td>
<td class="mono">${r.priority||0}</td>
<td><span class="badge ${r.is_active==='t'?'badge-green':'badge-red'}">${r.is_active==='t'?'ON':'OFF'}</span></td>
</tr>`).join('');
}
// ═══════ LOG ═══════
async function loadLog() {
const rows = await api('send_log');
document.getElementById('tbl-log').innerHTML = rows.map(r=>`<tr>
<td class="mono">${r.id}</td>
<td class="mono" style="font-size:11px">${r.recipient||'—'}</td>
<td>${r.subject||'—'}</td>
<td class="mono">${r.send_method||'—'}</td>
<td><span class="badge ${r.status==='sent'?'badge-green':'badge-red'}">${r.status}</span></td>
<td class="mono" style="font-size:11px">${r.created_at?.substring(11,19)||'—'}</td>
</tr>`).join('');
}
// ═══════ TEST SEND ═══════
async function testSend() {
const btn = document.getElementById('btn-test');
btn.disabled = true; btn.textContent = 'Sending...';
const r = await api('test_send', {
creative_id: document.getElementById('test-creative').value,
to: document.getElementById('test-to').value
});
btn.disabled = false; btn.textContent = 'Send Test Email';
document.getElementById('test-result').innerHTML = r.ok
? `<span style="color:var(--green)">✅ ${r.result}</span><br>Tracking: ${r.tracking_id}`
: `<span style="color:var(--red)">❌ ${r.result}</span>`;
}
function closeModal(id) { document.getElementById(id).classList.remove('show'); }
const loaders = {
dashboard: loadDashboard, creatives: loadCreatives, offers: loadOffers,
accounts: loadAccounts, brain: loadBrain, isp: loadIsp, methods: loadMethods,
log: loadLog, test: loadCreatives
};
// Init
loadDashboard();
</script>
</body>
</html>