591 lines
29 KiB
HTML
591 lines
29 KiB
HTML
<!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>
|