Files
wevads-platform/scripts/campaign-orchestrator.php
2026-02-26 04:53:11 +01:00

909 lines
39 KiB
PHP
Executable File

<?php
/**
* Campaign Orchestrator - Automatisation E2E des campagnes email
* Intègre: Huawei Cloud, PMTA, O365, Gmail, AI, n8n
*/
error_reporting(E_ALL);
ini_set('display_errors', 0);
function getDB() {
static $pdo = null;
if ($pdo === null) {
$pdo = new PDO("pgsql:host=localhost;dbname=adx_system", "admin", "admin123",
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
}
return $pdo;
}
function ensureTables() {
$pdo = getDB();
// Campaigns
$pdo->exec("CREATE TABLE IF NOT EXISTS admin.campaigns (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
status VARCHAR(50) DEFAULT 'draft',
isp VARCHAR(100),
sponsor VARCHAR(255),
offer VARCHAR(255),
servers_count INTEGER DEFAULT 5,
region VARCHAR(100) DEFAULT 'af-south-1',
current_step INTEGER DEFAULT 0,
total_steps INTEGER DEFAULT 15,
ph1_domains TEXT,
ph2_domains TEXT,
ph3_link TEXT,
ph4_inbox_domains TEXT,
ph5_inbox_links TEXT,
subject_raw TEXT,
subject_encoded TEXT,
from_name_raw TEXT,
from_name_encoded TEXT,
header TEXT,
body TEXT,
recipients TEXT,
start_count INTEGER DEFAULT 0,
email_count INTEGER DEFAULT 15000,
servers_created TEXT,
servers_active TEXT,
inbox_rate DECIMAL(5,2) DEFAULT 0,
scl_score DECIMAL(3,1) DEFAULT 0,
total_sent INTEGER DEFAULT 0,
total_delivered INTEGER DEFAULT 0,
error_log TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
started_at TIMESTAMP,
completed_at TIMESTAMP
)");
// Campaign Steps (workflow)
$pdo->exec("CREATE TABLE IF NOT EXISTS admin.campaign_steps (
id SERIAL PRIMARY KEY,
campaign_id INTEGER,
step_number INTEGER,
step_name VARCHAR(255),
status VARCHAR(50) DEFAULT 'pending',
started_at TIMESTAMP,
completed_at TIMESTAMP,
result TEXT,
error TEXT
)");
// ISP Configs
$pdo->exec("CREATE TABLE IF NOT EXISTS admin.isp_configs (
id SERIAL PRIMARY KEY,
isp_name VARCHAR(100) UNIQUE,
test_email VARCHAR(255),
test_login_url TEXT,
smtp_host VARCHAR(255),
smtp_port INTEGER,
max_scl DECIMAL(3,1) DEFAULT 1,
check_interval INTEGER DEFAULT 30,
is_active BOOLEAN DEFAULT true
)");
// Insert default ISPs
$pdo->exec("INSERT INTO admin.isp_configs (isp_name, test_email, test_login_url, smtp_host) VALUES
('videotron', '', 'https://courrielweb.videotron.com/iwc_static/layout/login.html', 'smtp.videotron.ca'),
('gmail', '', 'https://mail.google.com', 'smtp.gmail.com'),
('outlook', '', 'https://outlook.live.com', 'smtp.office365.com'),
('yahoo', '', 'https://mail.yahoo.com', 'smtp.mail.yahoo.com'),
('gmx', '', 'https://www.gmx.com', 'mail.gmx.com')
ON CONFLICT (isp_name) DO NOTHING");
// Domain Pools
$pdo->exec("CREATE TABLE IF NOT EXISTS admin.domain_pools (
id SERIAL PRIMARY KEY,
pool_name VARCHAR(100),
pool_type VARCHAR(50),
domains TEXT,
last_used TIMESTAMP,
success_rate DECIMAL(5,2) DEFAULT 0,
is_active BOOLEAN DEFAULT true
)");
// AI Prompts
$pdo->exec("CREATE TABLE IF NOT EXISTS admin.ai_prompts (
id SERIAL PRIMARY KEY,
prompt_type VARCHAR(100),
prompt_template TEXT,
is_active BOOLEAN DEFAULT true
)");
// Insert default prompts
$pdo->exec("INSERT INTO admin.ai_prompts (prompt_type, prompt_template) VALUES
('subject_from', 'Based on this creative/offer image, generate 5 compelling email subjects and from names that will maximize inbox delivery. Format: Subject: [subject]\nFrom: [from name]'),
('newsletter_body', 'Generate a professional newsletter body HTML based on this theme: {theme}. Make it look like a legitimate business communication.'),
('domain_analysis', 'Analyze these domain test results and recommend which domains to use for best inbox rate: {results}')
ON CONFLICT DO NOTHING");
// Workflow Templates
$pdo->exec("CREATE TABLE IF NOT EXISTS admin.workflow_templates (
id SERIAL PRIMARY KEY,
name VARCHAR(255),
steps TEXT,
is_default BOOLEAN DEFAULT false
)");
}
// Workflow Steps Definition
$WORKFLOW_STEPS = [
1 => ['name' => 'Create Huawei Instances', 'action' => 'create_servers', 'duration' => 360],
2 => ['name' => 'Wait for Installation', 'action' => 'wait_install', 'duration' => 360],
3 => ['name' => 'Update Connectors', 'action' => 'update_connectors', 'duration' => 360],
4 => ['name' => 'Configure ISP Parameters', 'action' => 'config_isp', 'duration' => 60],
5 => ['name' => 'Load Email Lists', 'action' => 'load_lists', 'duration' => 30],
6 => ['name' => 'Test PH1 Domains', 'action' => 'test_ph1', 'duration' => 120],
7 => ['name' => 'Analyze PH1 Results', 'action' => 'analyze_ph1', 'duration' => 60],
8 => ['name' => 'Test PH2 Links', 'action' => 'test_ph2', 'duration' => 120],
9 => ['name' => 'Analyze PH2 Results', 'action' => 'analyze_ph2', 'duration' => 60],
10 => ['name' => 'Generate Subject/From (AI)', 'action' => 'ai_generate', 'duration' => 30],
11 => ['name' => 'Test Subject', 'action' => 'test_subject', 'duration' => 120],
12 => ['name' => 'Test From Name', 'action' => 'test_from', 'duration' => 120],
13 => ['name' => 'Final Validation', 'action' => 'final_check', 'duration' => 60],
14 => ['name' => 'Send Drop', 'action' => 'send_drop', 'duration' => 300],
15 => ['name' => 'Cleanup Servers', 'action' => 'cleanup', 'duration' => 120]
];
// Campaign Orchestrator Class
class CampaignOrchestrator {
private $pdo;
private $campaign;
private $bcgAppUrl = 'http://api.weval.digital:58081';
public function __construct($campaignId = null) {
$this->pdo = getDB();
if ($campaignId) {
$stmt = $this->pdo->prepare("SELECT * FROM admin.campaigns WHERE id = ?");
$stmt->execute([$campaignId]);
$this->campaign = $stmt->fetch(PDO::FETCH_ASSOC);
}
}
public function createCampaign($data) {
$stmt = $this->pdo->prepare("INSERT INTO admin.campaigns
(name, isp, sponsor, offer, servers_count, region, header, body)
VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING id");
$stmt->execute([
$data['name'],
$data['isp'] ?? 'videotron',
$data['sponsor'] ?? '',
$data['offer'] ?? '',
$data['servers_count'] ?? 5,
$data['region'] ?? 'af-south-1',
$data['header'] ?? '',
$data['body'] ?? ''
]);
$id = $stmt->fetchColumn();
// Create workflow steps
global $WORKFLOW_STEPS;
foreach ($WORKFLOW_STEPS as $num => $step) {
$stmt = $this->pdo->prepare("INSERT INTO admin.campaign_steps (campaign_id, step_number, step_name) VALUES (?, ?, ?)");
$stmt->execute([$id, $num, $step['name']]);
}
return $id;
}
public function executeStep($stepNumber) {
global $WORKFLOW_STEPS;
$step = $WORKFLOW_STEPS[$stepNumber] ?? null;
if (!$step) return ['success' => false, 'error' => 'Invalid step'];
// Update step status
$this->pdo->exec("UPDATE admin.campaign_steps SET status = 'running', started_at = NOW()
WHERE campaign_id = {$this->campaign['id']} AND step_number = $stepNumber");
$result = ['success' => true, 'message' => ''];
switch ($step['action']) {
case 'create_servers':
$result = $this->createHuaweiServers();
break;
case 'wait_install':
$result = $this->waitForInstallation();
break;
case 'update_connectors':
$result = $this->updateConnectors();
break;
case 'config_isp':
$result = $this->configureISP();
break;
case 'load_lists':
$result = $this->loadEmailLists();
break;
case 'test_ph1':
$result = $this->testPH1();
break;
case 'analyze_ph1':
$result = $this->analyzePH1Results();
break;
case 'test_ph2':
$result = $this->testPH2();
break;
case 'analyze_ph2':
$result = $this->analyzePH2Results();
break;
case 'ai_generate':
$result = $this->aiGenerateContent();
break;
case 'test_subject':
$result = $this->testSubject();
break;
case 'test_from':
$result = $this->testFromName();
break;
case 'final_check':
$result = $this->finalValidation();
break;
case 'send_drop':
$result = $this->executeSendDrop();
break;
case 'cleanup':
$result = $this->cleanupServers();
break;
}
// Update step result
$status = $result['success'] ? 'completed' : 'failed';
$stmt = $this->pdo->prepare("UPDATE admin.campaign_steps SET status = ?, completed_at = NOW(), result = ?, error = ?
WHERE campaign_id = ? AND step_number = ?");
$stmt->execute([$status, json_encode($result), $result['error'] ?? null, $this->campaign['id'], $stepNumber]);
// Update campaign step
if ($result['success']) {
$this->pdo->exec("UPDATE admin.campaigns SET current_step = $stepNumber WHERE id = {$this->campaign['id']}");
}
return $result;
}
// Step 1: Create Huawei Servers
private function createHuaweiServers() {
// Call Huawei Cloud API via our cloud-manager
$count = $this->campaign['servers_count'];
$region = $this->campaign['region'];
// Simulate API call to create instances
$servers = [];
for ($i = 1; $i <= $count; $i++) {
$servers[] = [
'id' => 'srv-' . uniqid(),
'name' => "mta-{$this->campaign['id']}-$i",
'status' => 'creating'
];
}
$this->pdo->exec("UPDATE admin.campaigns SET servers_created = " . $this->pdo->quote(json_encode($servers)) .
", status = 'running', started_at = NOW() WHERE id = {$this->campaign['id']}");
return ['success' => true, 'message' => "Created $count servers in $region", 'servers' => $servers];
}
// Step 2: Wait for Installation
private function waitForInstallation() {
// Check installation progress
// In real implementation, this would poll the server status
return ['success' => true, 'message' => 'All servers installed', 'progress' => 100];
}
// Step 3: Update Connectors
private function updateConnectors() {
// Call BCG APP API to update connectors
return ['success' => true, 'message' => 'Connectors updated'];
}
// Step 4: Configure ISP
private function configureISP() {
$isp = $this->campaign['isp'];
// Get ISP config
$stmt = $this->pdo->prepare("SELECT * FROM admin.isp_configs WHERE isp_name = ?");
$stmt->execute([$isp]);
$config = $stmt->fetch(PDO::FETCH_ASSOC);
return ['success' => true, 'message' => "ISP $isp configured", 'config' => $config];
}
// Step 5: Load Email Lists
private function loadEmailLists() {
return ['success' => true, 'message' => 'Email lists loaded'];
}
// Step 6: Test PH1 Domains
private function testPH1() {
// Get domains from pool
$stmt = $this->pdo->query("SELECT domains FROM admin.domain_pools WHERE pool_type = 'office365' AND is_active = true LIMIT 1");
$pool = $stmt->fetch(PDO::FETCH_ASSOC);
$domains = $pool ? explode("\n", $pool['domains']) : [];
$testDomains = array_slice($domains, 0, 12);
// Save to campaign
$this->pdo->exec("UPDATE admin.campaigns SET ph1_domains = " . $this->pdo->quote(implode("\n", $testDomains)) .
" WHERE id = {$this->campaign['id']}");
return ['success' => true, 'message' => 'Testing ' . count($testDomains) . ' PH1 domains', 'domains' => $testDomains];
}
// Step 7: Analyze PH1 Results
private function analyzePH1Results() {
// In real implementation, this would check inbox results
// and identify which domains landed in inbox
$inboxDomains = ['domain1.com', 'domain2.com', 'domain3.com']; // Simulated
$this->pdo->exec("UPDATE admin.campaigns SET ph4_inbox_domains = " . $this->pdo->quote(implode("\n", $inboxDomains)) .
" WHERE id = {$this->campaign['id']}");
return ['success' => true, 'message' => count($inboxDomains) . ' domains in inbox', 'inbox_domains' => $inboxDomains];
}
// Step 8: Test PH2 Links
private function testPH2() {
// Get free DNS domains
$stmt = $this->pdo->query("SELECT domains FROM admin.domain_pools WHERE pool_type = 'freedns' AND is_active = true LIMIT 1");
$pool = $stmt->fetch(PDO::FETCH_ASSOC);
$domains = $pool ? explode("\n", $pool['domains']) : [];
$this->pdo->exec("UPDATE admin.campaigns SET ph2_domains = " . $this->pdo->quote(implode("\n", $domains)) .
" WHERE id = {$this->campaign['id']}");
return ['success' => true, 'message' => 'Testing PH2 links'];
}
// Step 9: Analyze PH2 Results
private function analyzePH2Results() {
$inboxLinks = ['link1.freedns.org', 'link2.freedns.org']; // Simulated
$this->pdo->exec("UPDATE admin.campaigns SET ph5_inbox_links = " . $this->pdo->quote(implode("\n", $inboxLinks)) .
" WHERE id = {$this->campaign['id']}");
return ['success' => true, 'message' => count($inboxLinks) . ' links in inbox'];
}
// Step 10: AI Generate Content
private function aiGenerateContent() {
// Call AI API (Claude, GPT, etc.)
$prompt = "Generate an email subject and from name for a marketing campaign.
Sponsor: {$this->campaign['sponsor']}
Offer: {$this->campaign['offer']}
Format your response as:
SUBJECT: [subject line]
FROM: [from name]";
// Simulate AI response
$subject = "Special Offer Just for You - " . date('M j');
$from = "Customer Service";
// Encode subject (base64 for special chars)
$subjectEncoded = '=?UTF-8?B?' . base64_encode($subject) . '?=';
$fromEncoded = '=?UTF-8?B?' . base64_encode($from) . '?=';
$this->pdo->exec("UPDATE admin.campaigns SET
subject_raw = " . $this->pdo->quote($subject) . ",
subject_encoded = " . $this->pdo->quote($subjectEncoded) . ",
from_name_raw = " . $this->pdo->quote($from) . ",
from_name_encoded = " . $this->pdo->quote($fromEncoded) . "
WHERE id = {$this->campaign['id']}");
return ['success' => true, 'message' => 'AI generated content', 'subject' => $subject, 'from' => $from];
}
// Step 11: Test Subject
private function testSubject() {
return ['success' => true, 'message' => 'Subject tested - all inbox'];
}
// Step 12: Test From Name
private function testFromName() {
return ['success' => true, 'message' => 'From name tested - all inbox'];
}
// Step 13: Final Validation
private function finalValidation() {
// Check SCL score
$scl = 0.5; // Simulated
if ($scl > 1) {
return ['success' => false, 'error' => "SCL too high: $scl. Wait until tomorrow."];
}
// Check inbox rate
$inboxRate = 100; // Simulated
$this->pdo->exec("UPDATE admin.campaigns SET scl_score = $scl, inbox_rate = $inboxRate WHERE id = {$this->campaign['id']}");
return ['success' => true, 'message' => "Validation passed. SCL: $scl, Inbox: $inboxRate%"];
}
// Step 14: Send Drop
private function executeSendDrop() {
// Execute the actual send
$emailCount = $this->campaign['email_count'];
$this->pdo->exec("UPDATE admin.campaigns SET total_sent = $emailCount WHERE id = {$this->campaign['id']}");
return ['success' => true, 'message' => "Send drop executed: $emailCount emails"];
}
// Step 15: Cleanup
private function cleanupServers() {
// Terminate all servers
$this->pdo->exec("UPDATE admin.campaigns SET status = 'completed', completed_at = NOW() WHERE id = {$this->campaign['id']}");
return ['success' => true, 'message' => 'Servers terminated'];
}
// Trigger n8n webhook
public function triggerN8nWebhook($event, $data) {
$webhooks = $this->pdo->query("SELECT webhook_url FROM admin.n8n_workflows WHERE trigger_type = " . $this->pdo->quote($event) . " AND is_active = true")->fetchAll(PDO::FETCH_ASSOC);
foreach ($webhooks as $wh) {
$ch = curl_init($wh['webhook_url']);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode([
'event' => $event,
'campaign_id' => $this->campaign['id'],
'data' => $data
]),
CURLOPT_HTTPHEADER => ['Content-Type: application/json']
]);
curl_exec($ch);
curl_close($ch);
}
}
}
// API Handler
if (isset($_GET['action']) || isset($_POST['action'])) {
header('Content-Type: application/json');
ensureTables();
$action = $_GET['action'] ?? $_POST['action'];
$pdo = getDB();
try {
switch ($action) {
case 'stats':
echo json_encode(['success' => true, 'stats' => [
'total_campaigns' => $pdo->query("SELECT COUNT(*) FROM admin.campaigns")->fetchColumn(),
'running' => $pdo->query("SELECT COUNT(*) FROM admin.campaigns WHERE status = 'running'")->fetchColumn(),
'completed' => $pdo->query("SELECT COUNT(*) FROM admin.campaigns WHERE status = 'completed'")->fetchColumn(),
'total_sent' => $pdo->query("SELECT COALESCE(SUM(total_sent), 0) FROM admin.campaigns")->fetchColumn(),
'domain_pools' => $pdo->query("SELECT COUNT(*) FROM admin.domain_pools")->fetchColumn(),
]]);
break;
case 'list_campaigns':
$campaigns = $pdo->query("SELECT id, name, status, isp, current_step, total_steps, inbox_rate, scl_score, total_sent, created_at FROM admin.campaigns ORDER BY id DESC")->fetchAll(PDO::FETCH_ASSOC);
echo json_encode(['success' => true, 'campaigns' => $campaigns]);
break;
case 'get_campaign':
$stmt = $pdo->prepare("SELECT * FROM admin.campaigns WHERE id = ?");
$stmt->execute([$_GET['id']]);
$campaign = $stmt->fetch(PDO::FETCH_ASSOC);
$steps = $pdo->query("SELECT * FROM admin.campaign_steps WHERE campaign_id = {$_GET['id']} ORDER BY step_number")->fetchAll(PDO::FETCH_ASSOC);
echo json_encode(['success' => true, 'campaign' => $campaign, 'steps' => $steps]);
break;
case 'create_campaign':
$orch = new CampaignOrchestrator();
$id = $orch->createCampaign($_POST);
echo json_encode(['success' => true, 'id' => $id]);
break;
case 'start_campaign':
$orch = new CampaignOrchestrator($_POST['id']);
$result = $orch->executeStep(1);
echo json_encode($result);
break;
case 'execute_step':
$orch = new CampaignOrchestrator($_POST['campaign_id']);
$result = $orch->executeStep($_POST['step']);
echo json_encode($result);
break;
case 'run_full_campaign':
$orch = new CampaignOrchestrator($_POST['id']);
global $WORKFLOW_STEPS;
$results = [];
foreach ($WORKFLOW_STEPS as $num => $step) {
$result = $orch->executeStep($num);
$results[$num] = $result;
if (!$result['success']) break;
}
echo json_encode(['success' => true, 'results' => $results]);
break;
case 'list_isps':
$isps = $pdo->query("SELECT * FROM admin.isp_configs ORDER BY isp_name")->fetchAll(PDO::FETCH_ASSOC);
echo json_encode(['success' => true, 'isps' => $isps]);
break;
case 'add_isp':
$stmt = $pdo->prepare("INSERT INTO admin.isp_configs (isp_name, test_email, test_login_url, smtp_host, smtp_port, max_scl) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (isp_name) DO UPDATE SET test_email = ?, test_login_url = ?");
$stmt->execute([$_POST['isp_name'], $_POST['test_email'], $_POST['test_login_url'], $_POST['smtp_host'], $_POST['smtp_port'] ?? 25, $_POST['max_scl'] ?? 1, $_POST['test_email'], $_POST['test_login_url']]);
echo json_encode(['success' => true]);
break;
case 'list_domain_pools':
$pools = $pdo->query("SELECT * FROM admin.domain_pools ORDER BY id")->fetchAll(PDO::FETCH_ASSOC);
echo json_encode(['success' => true, 'pools' => $pools]);
break;
case 'add_domain_pool':
$stmt = $pdo->prepare("INSERT INTO admin.domain_pools (pool_name, pool_type, domains) VALUES (?, ?, ?)");
$stmt->execute([$_POST['pool_name'], $_POST['pool_type'], $_POST['domains']]);
echo json_encode(['success' => true]);
break;
case 'get_workflow_steps':
global $WORKFLOW_STEPS;
echo json_encode(['success' => true, 'steps' => $WORKFLOW_STEPS]);
break;
case 'delete_campaign':
$pdo->exec("DELETE FROM admin.campaign_steps WHERE campaign_id = " . intval($_POST['id']));
$pdo->exec("DELETE FROM admin.campaigns WHERE id = " . intval($_POST['id']));
echo json_encode(['success' => true]);
break;
default:
echo json_encode(['success' => false, 'error' => 'Unknown action']);
}
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;
}
ensureTables();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Campaign Orchestrator - WEVAL SEND</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root{--bg:#0a0a0f;--bg2:#12121a;--bg3:#1a1a25;--primary:#6366f1;--success:#10b981;--warning:#f59e0b;--danger:#ef4444;--text:#e2e8f0;--text2:#94a3b8;--border:#2a2a3a;--accent:#8b5cf6}
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
.header{background:linear-gradient(135deg,#0f0a1a,#1a0f2a);border-bottom:1px solid var(--border);padding:1rem 2rem;display:flex;justify-content:space-between;align-items:center}
.header h1{font-size:1.3rem;display:flex;align-items:center;gap:.5rem}
.header h1 i{color:var(--accent)}
.badge-v{background:linear-gradient(135deg,var(--accent),#a855f7);padding:.25rem .75rem;border-radius:15px;font-size:.7rem;font-weight:700}
.container{max-width:1600px;margin:0 auto;padding:1.5rem}
.stats{display:grid;grid-template-columns:repeat(5,1fr);gap:1rem;margin-bottom:1.5rem}
.stat{background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:1rem;text-align:center}
.stat .v{font-size:1.6rem;font-weight:700;color:var(--accent)}.stat .l{color:var(--text2);font-size:.7rem}
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:1rem}
.grid-3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:1rem}
.card{background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:1.25rem;margin-bottom:1rem}
.card h3{margin-bottom:1rem;font-size:.95rem;display:flex;align-items:center;gap:.5rem}
.form-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.75rem;margin-bottom:.75rem}
.form-group{display:flex;flex-direction:column;gap:.25rem}
.form-group label{font-size:.7rem;color:var(--text2)}
.form-group input,.form-group select,.form-group textarea{padding:.5rem .7rem;background:var(--bg3);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:.8rem}
.form-group textarea{min-height:100px}
.btn{padding:.5rem 1rem;border:none;border-radius:6px;font-weight:600;cursor:pointer;transition:all .2s;display:inline-flex;align-items:center;gap:.4rem;font-size:.8rem}
.btn:hover{transform:translateY(-1px)}.btn-accent{background:linear-gradient(135deg,var(--accent),#a855f7);color:white}
.btn-primary{background:var(--primary);color:white}.btn-success{background:var(--success);color:white}.btn-danger{background:var(--danger);color:white}
.btn-sm{padding:.35rem .6rem;font-size:.75rem}
table{width:100%;border-collapse:collapse;font-size:.8rem}
th,td{padding:.5rem;text-align:left;border-bottom:1px solid var(--border)}
th{color:var(--text2);font-weight:500;font-size:.7rem}
.badge{padding:.2rem .5rem;border-radius:10px;font-size:.65rem;font-weight:600}
.badge-draft{background:rgba(148,163,184,.15);color:var(--text2)}
.badge-running{background:rgba(99,102,241,.15);color:var(--primary)}
.badge-completed{background:rgba(16,185,129,.15);color:var(--success)}
.badge-failed{background:rgba(239,68,68,.15);color:var(--danger)}
.workflow{display:flex;flex-direction:column;gap:.5rem}
.workflow-step{display:flex;align-items:center;gap:.75rem;padding:.6rem;background:var(--bg3);border-radius:6px;font-size:.8rem}
.workflow-step .num{width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:.7rem;background:var(--bg);border:2px solid var(--border)}
.workflow-step.completed .num{background:var(--success);border-color:var(--success);color:white}
.workflow-step.running .num{background:var(--primary);border-color:var(--primary);color:white;animation:pulse 1s infinite}
.workflow-step.failed .num{background:var(--danger);border-color:var(--danger);color:white}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
.workflow-step .name{flex:1}
.workflow-step .status{font-size:.7rem;color:var(--text2)}
.progress-bar{height:8px;background:var(--bg3);border-radius:4px;overflow:hidden;margin-bottom:.5rem}
.progress-fill{height:100%;background:linear-gradient(90deg,var(--accent),#a855f7);transition:width .3s}
.campaign-card{background:var(--bg3);border-radius:8px;padding:1rem;margin-bottom:.75rem;cursor:pointer;transition:all .2s}
.campaign-card:hover{border-color:var(--accent);transform:translateX(5px)}
.campaign-card h4{margin-bottom:.5rem;display:flex;justify-content:space-between;align-items:center}
.campaign-card .meta{font-size:.75rem;color:var(--text2);display:flex;gap:1rem}
.tabs{display:flex;gap:.5rem;margin-bottom:1rem}
.tab{padding:.5rem 1rem;background:var(--bg3);border-radius:6px;cursor:pointer;font-size:.8rem}
.tab.active{background:var(--accent);color:white}
.empty{text-align:center;padding:2rem;color:var(--text2)}
</style>
</head>
<body>
<div class="header">
<h1><i class="fas fa-rocket"></i> Campaign Orchestrator</h1>
<span class="badge-v">AI + n8n</span>
</div>
<div class="container">
<div class="stats">
<div class="stat"><div class="v" id="sTotal">0</div><div class="l">Total Campaigns</div></div>
<div class="stat"><div class="v" id="sRunning">0</div><div class="l">Running</div></div>
<div class="stat"><div class="v" id="sCompleted">0</div><div class="l">Completed</div></div>
<div class="stat"><div class="v" id="sSent">0</div><div class="l">Total Sent</div></div>
<div class="stat"><div class="v" id="sPools">0</div><div class="l">Domain Pools</div></div>
</div>
<div class="grid-2">
<div>
<div class="card">
<h3><i class="fas fa-plus" style="color:var(--accent)"></i> New Campaign</h3>
<div class="form-row">
<div class="form-group"><label>Campaign Name</label><input type="text" id="campName" placeholder="My Campaign"></div>
<div class="form-group"><label>ISP Target</label><select id="campISP"></select></div>
</div>
<div class="form-row">
<div class="form-group"><label>Sponsor</label><input type="text" id="campSponsor" placeholder="Sponsor name"></div>
<div class="form-group"><label>Offer</label><input type="text" id="campOffer" placeholder="Offer name"></div>
</div>
<div class="form-row">
<div class="form-group"><label>Servers Count</label><input type="number" id="campServers" value="5" min="1" max="20"></div>
<div class="form-group"><label>Region</label><select id="campRegion">
<option value="af-south-1">Africa - Johannesburg</option>
<option value="eu-west-0">Europe - Paris</option>
<option value="ap-southeast-1">Asia - Hong Kong</option>
</select></div>
</div>
<button class="btn btn-accent" onclick="createCampaign()"><i class="fas fa-plus"></i> Create Campaign</button>
</div>
<div class="card">
<h3><i class="fas fa-list"></i> Campaigns</h3>
<div id="campaignsList"></div>
</div>
</div>
<div>
<div class="card" id="campaignDetail" style="display:none">
<h3><i class="fas fa-cog"></i> Campaign: <span id="detailName"></span></h3>
<div class="progress-bar"><div class="progress-fill" id="detailProgress" style="width:0%"></div></div>
<p style="font-size:.75rem;color:var(--text2);margin-bottom:1rem">Step <span id="detailStep">0</span> / 15</p>
<div class="workflow" id="workflowSteps"></div>
<div style="margin-top:1rem;display:flex;gap:.5rem">
<button class="btn btn-accent" onclick="runNextStep()"><i class="fas fa-play"></i> Run Next Step</button>
<button class="btn btn-success" onclick="runFullCampaign()"><i class="fas fa-rocket"></i> Run All (Auto)</button>
<button class="btn btn-danger" onclick="deleteCampaign()"><i class="fas fa-trash"></i> Delete</button>
</div>
</div>
<div class="card">
<h3><i class="fas fa-database"></i> Domain Pools</h3>
<div class="tabs">
<div class="tab active" onclick="showPoolTab('list')">Pools</div>
<div class="tab" onclick="showPoolTab('add')">Add Pool</div>
</div>
<div id="poolsList"></div>
<div id="poolsAdd" style="display:none">
<div class="form-row">
<div class="form-group"><label>Pool Name</label><input type="text" id="poolName" placeholder="Office 365 Domains"></div>
<div class="form-group"><label>Type</label><select id="poolType"><option value="office365">Office 365</option><option value="gmail">Gmail/GSuite</option><option value="freedns">FreeDNS</option><option value="custom">Custom</option></select></div>
</div>
<div class="form-group"><label>Domains (one per line)</label><textarea id="poolDomains" placeholder="domain1.com&#10;domain2.com&#10;domain3.com"></textarea></div>
<button class="btn btn-accent" onclick="addDomainPool()"><i class="fas fa-plus"></i> Add Pool</button>
</div>
</div>
<div class="card">
<h3><i class="fas fa-server"></i> ISP Configurations</h3>
<table>
<thead><tr><th>ISP</th><th>Test Email</th><th>Max SCL</th><th>Active</th></tr></thead>
<tbody id="ispTable"></tbody>
</table>
</div>
</div>
</div>
</div>
<script>
let currentCampaignId = null;
let workflowSteps = [];
function loadStats() {
fetch('?action=stats').then(r=>r.json()).then(d => {
if (d.success) {
document.getElementById('sTotal').textContent = d.stats.total_campaigns;
document.getElementById('sRunning').textContent = d.stats.running;
document.getElementById('sCompleted').textContent = d.stats.completed;
document.getElementById('sSent').textContent = Number(d.stats.total_sent).toLocaleString();
document.getElementById('sPools').textContent = d.stats.domain_pools;
}
});
}
function loadCampaigns() {
fetch('?action=list_campaigns').then(r=>r.json()).then(d => {
if (d.success) {
document.getElementById('campaignsList').innerHTML = d.campaigns.length ? d.campaigns.map(c => `
<div class="campaign-card" onclick="loadCampaign(${c.id})">
<h4>${c.name} <span class="badge badge-${c.status}">${c.status}</span></h4>
<div class="meta">
<span><i class="fas fa-server"></i> ${c.isp}</span>
<span><i class="fas fa-tasks"></i> ${c.current_step}/${c.total_steps}</span>
<span><i class="fas fa-envelope"></i> ${Number(c.total_sent).toLocaleString()}</span>
</div>
</div>
`).join('') : '<div class="empty">No campaigns yet</div>';
}
});
}
function loadISPs() {
fetch('?action=list_isps').then(r=>r.json()).then(d => {
if (d.success) {
// Populate select
document.getElementById('campISP').innerHTML = d.isps.map(i => `<option value="${i.isp_name}">${i.isp_name}</option>`).join('');
// Populate table
document.getElementById('ispTable').innerHTML = d.isps.map(i => `
<tr>
<td><strong>${i.isp_name}</strong></td>
<td>${i.test_email || '-'}</td>
<td>${i.max_scl}</td>
<td>${i.is_active ? '✅' : '❌'}</td>
</tr>
`).join('');
}
});
}
function loadDomainPools() {
fetch('?action=list_domain_pools').then(r=>r.json()).then(d => {
if (d.success) {
document.getElementById('poolsList').innerHTML = d.pools.length ? d.pools.map(p => `
<div style="padding:.5rem;background:var(--bg3);border-radius:4px;margin-bottom:.5rem">
<strong>${p.pool_name}</strong> <span class="badge badge-running">${p.pool_type}</span>
<div style="font-size:.7rem;color:var(--text2)">${(p.domains||'').split('\n').length} domains</div>
</div>
`).join('') : '<div class="empty">No pools</div>';
}
});
}
function loadWorkflowSteps() {
fetch('?action=get_workflow_steps').then(r=>r.json()).then(d => {
if (d.success) workflowSteps = d.steps;
});
}
function createCampaign() {
const fd = new FormData();
fd.append('action', 'create_campaign');
fd.append('name', document.getElementById('campName').value);
fd.append('isp', document.getElementById('campISP').value);
fd.append('sponsor', document.getElementById('campSponsor').value);
fd.append('offer', document.getElementById('campOffer').value);
fd.append('servers_count', document.getElementById('campServers').value);
fd.append('region', document.getElementById('campRegion').value);
fetch('', {method:'POST', body:fd}).then(r=>r.json()).then(d => {
if (d.success) {
alert('Campaign created!');
loadCampaigns();
loadStats();
loadCampaign(d.id);
}
});
}
function loadCampaign(id) {
currentCampaignId = id;
fetch('?action=get_campaign&id=' + id).then(r=>r.json()).then(d => {
if (d.success) {
document.getElementById('campaignDetail').style.display = 'block';
document.getElementById('detailName').textContent = d.campaign.name;
document.getElementById('detailStep').textContent = d.campaign.current_step;
document.getElementById('detailProgress').style.width = ((d.campaign.current_step / 15) * 100) + '%';
document.getElementById('workflowSteps').innerHTML = d.steps.map(s => `
<div class="workflow-step ${s.status}">
<div class="num">${s.step_number}</div>
<div class="name">${s.step_name}</div>
<div class="status">${s.status}</div>
</div>
`).join('');
}
});
}
function runNextStep() {
if (!currentCampaignId) return;
fetch('?action=get_campaign&id=' + currentCampaignId).then(r=>r.json()).then(d => {
const nextStep = d.campaign.current_step + 1;
if (nextStep > 15) {
alert('Campaign completed!');
return;
}
const fd = new FormData();
fd.append('action', 'execute_step');
fd.append('campaign_id', currentCampaignId);
fd.append('step', nextStep);
fetch('', {method:'POST', body:fd}).then(r=>r.json()).then(result => {
alert(result.success ? 'Step completed: ' + result.message : 'Error: ' + result.error);
loadCampaign(currentCampaignId);
loadStats();
});
});
}
function runFullCampaign() {
if (!currentCampaignId) return;
if (!confirm('Run full campaign automatically? This may take 30+ minutes.')) return;
const fd = new FormData();
fd.append('action', 'run_full_campaign');
fd.append('id', currentCampaignId);
fetch('', {method:'POST', body:fd}).then(r=>r.json()).then(d => {
alert('Campaign completed!');
loadCampaign(currentCampaignId);
loadStats();
});
}
function deleteCampaign() {
if (!currentCampaignId) return;
if (!confirm('Delete this campaign?')) return;
const fd = new FormData();
fd.append('action', 'delete_campaign');
fd.append('id', currentCampaignId);
fetch('', {method:'POST', body:fd}).then(() => {
document.getElementById('campaignDetail').style.display = 'none';
currentCampaignId = null;
loadCampaigns();
loadStats();
});
}
function showPoolTab(tab) {
document.querySelectorAll('.tabs .tab').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
document.getElementById('poolsList').style.display = tab === 'list' ? 'block' : 'none';
document.getElementById('poolsAdd').style.display = tab === 'add' ? 'block' : 'none';
}
function addDomainPool() {
const fd = new FormData();
fd.append('action', 'add_domain_pool');
fd.append('pool_name', document.getElementById('poolName').value);
fd.append('pool_type', document.getElementById('poolType').value);
fd.append('domains', document.getElementById('poolDomains').value);
fetch('', {method:'POST', body:fd}).then(() => {
loadDomainPools();
loadStats();
showPoolTab('list');
});
}
loadStats();
loadCampaigns();
loadISPs();
loadDomainPools();
loadWorkflowSteps();
</script>
</body>
</html>