Files
wevia-brain/s89-arsenal-screens/ethica-hcp-manager.html
2026-04-12 23:01:36 +02:00

595 lines
35 KiB
HTML
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
$db = new PDO("pgsql:host=localhost;dbname=adx_system", "admin", "admin123");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$total = (int)$db->query("SELECT COUNT(*) FROM ethica.medecins")->fetchColumn();
$valid_stats = $db->query("SELECT email_valid, COUNT(*) c FROM ethica.medecins GROUP BY email_valid ORDER BY c DESC")->fetchAll(PDO::FETCH_KEY_PAIR);
$consent_stats = $db->query("SELECT consent_status, COUNT(*) c FROM ethica.medecins GROUP BY consent_status ORDER BY c DESC")->fetchAll(PDO::FETCH_KEY_PAIR);
$running_val = (int)trim(shell_exec("pgrep -f 'ethica-validator' 2>/dev/null | wc -l") ?? '0');
$running_scr = (int)trim(shell_exec("pgrep -f 'ethica-mega\|ethica-scraper' 2>/dev/null | wc -l") ?? '0');
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Ethica HCP — Validator · Consent · Scraping</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root{--bg:#0f172a;--bg2:#1e293b;--bg3:#334155;--border:#475569;--text:#f1f5f9;--text2:#94a3b8;--text3:#64748b;
--teal:#14b8a6;--blue:#3b82f6;--purple:#a855f7;--orange:#f59e0b;--red:#ef4444;--green:#22c55e;--cyan:#06b6d4;--pink:#ec4899;}
*{margin:0;padding:0;box-sizing:border-box;}
body{background:var(--bg);color:var(--text);font-family:'Inter',sans-serif;min-height:100vh;}
.hdr{background:linear-gradient(135deg,#0d9488,#065f46);padding:14px 24px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid rgba(255,255,255,.1);}
.hdr-left{display:flex;align-items:center;gap:10px;}
.hdr h1{font-size:17px;font-weight:800;}
.hdr-sub{font-size:10px;opacity:.7;}
.hdr-right{display:flex;gap:8px;align-items:center;}
.chip{background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.15);border-radius:14px;padding:3px 10px;font-size:10px;font-weight:600;display:flex;align-items:center;gap:4px;}
.chip .dot{width:6px;height:6px;border-radius:50%;animation:pulse 2s infinite;}
.dot-green{background:var(--green);}.dot-orange{background:var(--orange);animation-duration:.6s!important;}
@keyframes pulse{50%{opacity:.3;}}
.tabs{display:flex;background:var(--bg2);border-bottom:1px solid rgba(255,255,255,.05);overflow-x:auto;}
.tab{padding:11px 18px;font-size:12px;font-weight:700;color:var(--text3);cursor:pointer;border-bottom:2px solid transparent;white-space:nowrap;transition:all .15s;}
.tab:hover{color:var(--text);background:rgba(255,255,255,.02);}
.tab.active{color:var(--teal);border-bottom-color:var(--teal);}
.content{max-width:1500px;margin:0 auto;padding:20px 24px;}
.panel{display:none;}.panel.active{display:block;}
.stats-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:10px;margin-bottom:18px;}
.stat{background:var(--bg2);border-radius:10px;padding:12px 14px;border-left:3px solid var(--teal);}
.stat.blue{border-left-color:var(--blue);}.stat.green{border-left-color:var(--green);}.stat.red{border-left-color:var(--red);}
.stat.orange{border-left-color:var(--orange);}.stat.purple{border-left-color:var(--purple);}.stat.cyan{border-left-color:var(--cyan);}.stat.pink{border-left-color:var(--pink);}
.stat-lbl{font-size:9px;text-transform:uppercase;letter-spacing:.5px;color:var(--text3);font-weight:700;}
.stat-val{font-size:22px;font-weight:900;margin-top:2px;}
.card{background:var(--bg2);border-radius:10px;border:1px solid rgba(255,255,255,.05);overflow:hidden;margin-bottom:14px;}
.card-head{padding:10px 14px;border-bottom:1px solid rgba(255,255,255,.05);display:flex;align-items:center;justify-content:space-between;}
.card-head h3{font-size:13px;font-weight:700;display:flex;align-items:center;gap:6px;}
.card-body{padding:14px;}
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:14px;}
.grid-3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px;}
.tbl{width:100%;border-collapse:collapse;font-size:11px;}
.tbl th{text-align:left;padding:7px 8px;font-weight:700;color:var(--text3);font-size:9px;text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid var(--bg3);cursor:pointer;}
.tbl td{padding:7px 8px;border-bottom:1px solid rgba(255,255,255,.03);}
.tbl tr:hover td{background:rgba(255,255,255,.02);}
.badge{display:inline-block;padding:2px 7px;border-radius:6px;font-size:9px;font-weight:700;}
.b-valid{background:rgba(34,197,94,.15);color:#86efac;}.b-invalid{background:rgba(239,68,68,.15);color:#fca5a5;}
.b-risky{background:rgba(245,158,11,.15);color:#fcd34d;}.b-unknown{background:rgba(100,116,139,.15);color:#94a3b8;}
.b-catch{background:rgba(6,182,212,.15);color:#67e8f9;}
.b-pending{background:rgba(168,85,247,.15);color:#c4b5fd;}.b-optin{background:rgba(34,197,94,.15);color:#86efac;}
.b-optout{background:rgba(239,68,68,.15);color:#fca5a5;}.b-implicit{background:rgba(59,130,246,.15);color:#93c5fd;}
.bar-h{display:flex;align-items:center;gap:8px;margin-bottom:6px;}
.bar-lbl{font-size:11px;font-weight:600;min-width:80px;}
.bar-track{flex:1;height:7px;background:var(--bg);border-radius:4px;overflow:hidden;}
.bar-fill{height:100%;border-radius:4px;transition:width .4s;}
.bar-cnt{font-size:11px;font-weight:700;min-width:60px;text-align:right;}
.btn{padding:6px 14px;border:none;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;transition:all .15s;}
.btn-teal{background:var(--teal);color:#fff;}.btn-teal:hover{background:#0d9488;}
.btn-sm{padding:4px 10px;font-size:10px;}
.btn-ghost{background:transparent;border:1px solid var(--bg3);color:var(--text2);}.btn-ghost:hover{border-color:var(--teal);color:var(--teal);}
.btn-red{background:var(--red);color:#fff;}
.btn-row{display:flex;gap:6px;flex-wrap:wrap;margin-top:10px;}
.toolbar{display:flex;gap:6px;flex-wrap:wrap;align-items:center;margin-bottom:12px;}
.toolbar select,.toolbar input{background:var(--bg);border:1px solid var(--bg3);color:var(--text);padding:6px 10px;border-radius:6px;font-size:11px;font-family:inherit;}
.pager{display:flex;gap:3px;align-items:center;margin-top:10px;flex-wrap:wrap;}
.pager .btn.active{background:var(--teal);color:#fff;}
.pager-info{font-size:10px;color:var(--text3);margin-left:auto;}
.toast{position:fixed;top:16px;right:16px;background:var(--teal);color:#fff;padding:10px 18px;border-radius:8px;font-size:12px;font-weight:600;z-index:9999;display:none;box-shadow:0 4px 20px rgba(0,0,0,.3);}
.toast.show{display:block;animation:slideIn .3s;}
@keyframes slideIn{from{transform:translateX(80px);opacity:0;}}
/* Source checkboxes */
.src-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:6px;}
.src-item{display:flex;align-items:center;gap:8px;padding:8px 10px;background:var(--bg);border:1px solid var(--bg3);border-radius:8px;cursor:pointer;transition:all .15s;font-size:11px;}
.src-item:hover{border-color:var(--teal);background:rgba(20,184,166,.05);}
.src-item input{accent-color:var(--teal);width:14px;height:14px;}
.src-item .src-name{font-weight:600;flex:1;}
.src-item .src-type{font-size:9px;color:var(--text3);background:var(--bg2);padding:1px 6px;border-radius:4px;}
.src-item .src-pays{font-size:9px;}
.consent-box{background:var(--bg);border:1px solid var(--bg3);border-radius:10px;padding:16px;margin-bottom:12px;}
.consent-box h4{font-size:13px;font-weight:700;margin-bottom:8px;}
.consent-box p{font-size:12px;color:var(--text2);line-height:1.5;}
@media(max-width:900px){.grid-2,.grid-3{grid-template-columns:1fr;}.stats-row{grid-template-columns:repeat(2,1fr);}}
</style>
</head>
<body>
<div class="hdr">
<div class="hdr-left"><span style="font-size:22px;">💊</span><div><h1>Ethica HCP — Validator · Consent · Scraping</h1><div class="hdr-sub"><?=number_format($total)?> contacts — 10 spécialités prioritaires + 2 à venir — 3 pays</div></div></div>
<div class="hdr-right">
<div class="chip"><span class="dot <?=$running_val?'dot-orange':'dot-green'?>"></span><?=$running_val?'Validation...':'Validator idle'?></div>
<div class="chip"><span class="dot <?=$running_scr?'dot-orange':'dot-green'?>"></span><?=$running_scr?'Scraping...':'Scraper idle'?></div>
<a href="/ethica-dashboard.html" class="btn btn-sm btn-ghost"><i class="fas fa-arrow-left"></i> Dashboard</a>
<a href="/ethica-drill.html" class="btn btn-sm btn-ghost"><i class="fas fa-search"></i> Drill</a>
</div>
</div>
<div class="tabs">
<div class="tab active" onclick="showTab('validator')"><i class="fas fa-check-circle"></i> Email Validator</div>
<div class="tab" onclick="showTab('consent')"><i class="fas fa-shield-alt"></i> Consentement</div>
<div class="tab" onclick="showTab('scraping')"><i class="fas fa-spider"></i> Scraping Multi-Sources</div>
</div>
<div class="content">
<div class="toast" id="toast"></div>
<!-- ============ EMAIL VALIDATOR ============ -->
<div class="panel active" id="validator">
<div class="stats-row" id="val-stats">
<div class="stat green"><div class="stat-lbl">✅ Valid</div><div class="stat-val" id="sv-valid"><?=$valid_stats['valid']??0?></div></div>
<div class="stat red"><div class="stat-lbl">❌ Invalid</div><div class="stat-val" id="sv-invalid"><?=$valid_stats['invalid']??0?></div></div>
<div class="stat orange"><div class="stat-lbl">⚠️ Risky</div><div class="stat-val" id="sv-risky"><?=$valid_stats['risky']??0?></div></div>
<div class="stat cyan"><div class="stat-lbl">🔄 Catch-All</div><div class="stat-val" id="sv-catch"><?=$valid_stats['catch_all']??0?></div></div>
<div class="stat"><div class="stat-lbl">❓ Unknown</div><div class="stat-val" id="sv-unknown"><?=$valid_stats['unknown']??0?></div></div>
<div class="stat purple"><div class="stat-lbl">📊 Checked</div><div class="stat-val" id="sv-checked"><?=number_format(($valid_stats['valid']??0)+($valid_stats['invalid']??0)+($valid_stats['risky']??0)+($valid_stats['catch_all']??0))?></div></div>
</div>
<div class="grid-2">
<div class="card">
<div class="card-head"><h3><i class="fas fa-play-circle"></i> Lancer Validation</h3></div>
<div class="card-body">
<div class="toolbar">
<select id="vf-pays"><option value="">Tous pays</option><option value="MA">🇲🇦 Maroc</option><option value="TN">🇹🇳 Tunisie</option><option value="DZ">🇩🇿 Algérie</option></select>
<select id="vf-batch">
<option value="100">100 emails</option><option value="500" selected>500 emails</option>
<option value="1000">1,000 emails</option><option value="2000">2,000 emails</option>
<option value="5000">5,000 emails</option><option value="10000">10,000 (loop)</option>
</select>
<button class="btn btn-teal" id="btn-validate" onclick="launchValidation()"><i class="fas fa-play"></i> Lancer</button>
<button class="btn btn-ghost" onclick="launchValidation(true)"><i class="fas fa-infinity"></i> Continu</button>
</div>
<div id="val-progress" style="font-size:12px;color:var(--text2);margin-top:8px;"></div>
<div style="margin-top:14px;">
<h4 style="font-size:12px;font-weight:700;margin-bottom:8px;">Répartition</h4>
<div id="val-bars"></div>
</div>
</div>
</div>
<div class="card">
<div class="card-head"><h3><i class="fas fa-history"></i> Historique Batches</h3></div>
<div class="card-body" style="max-height:400px;overflow-y:auto;" id="val-history">Chargement...</div>
</div>
</div>
<div class="card">
<div class="card-head">
<h3><i class="fas fa-list"></i> Résultats Détaillés</h3>
<div class="toolbar" style="margin:0;">
<select id="vl-filter" onchange="loadValList()">
<option value="">Tous statuts</option>
<option value="valid">✅ Valid</option>
<option value="invalid">❌ Invalid</option>
<option value="risky">⚠️ Risky</option>
<option value="catch_all">🔄 Catch-All</option>
<option value="unknown">❓ Unknown</option>
</select>
<input type="text" id="vl-search" placeholder="🔍 Email..." oninput="clearTimeout(window._vst);window._vst=setTimeout(loadValList,400);" style="width:200px;">
</div>
</div>
<div class="card-body" style="overflow-x:auto;" id="val-list">Chargement...</div>
<div class="pager" id="val-pager"></div>
</div>
</div>
<!-- ============ CONSENTEMENT ============ -->
<div class="panel" id="consent">
<div class="stats-row" id="con-stats">
<div class="stat purple"><div class="stat-lbl">⏳ Pending</div><div class="stat-val" id="sc-pending"><?=$consent_stats['pending']??$total?></div></div>
<div class="stat green"><div class="stat-lbl">✅ Opt-in</div><div class="stat-val" id="sc-optin"><?=$consent_stats['opt-in']??0?></div></div>
<div class="stat blue"><div class="stat-lbl">🔗 Implicite</div><div class="stat-val" id="sc-implicit"><?=$consent_stats['implicit']??0?></div></div>
<div class="stat red"><div class="stat-lbl">🚫 Opt-out</div><div class="stat-val" id="sc-optout"><?=$consent_stats['opt-out']??0?></div></div>
<div class="stat orange"><div class="stat-lbl">📧 Unsubscribed</div><div class="stat-val" id="sc-unsub"><?=$consent_stats['unsubscribed']??0?></div></div>
</div>
<div class="grid-2">
<div class="card">
<div class="card-head"><h3><i class="fas fa-shield-alt"></i> Système de Consentement</h3></div>
<div class="card-body">
<div class="consent-box">
<h4>📋 Texte de Consentement Officiel</h4>
<div style="background:rgba(13,148,136,.08);border:1px solid rgba(13,148,136,.25);border-radius:8px;padding:14px;margin:8px 0;font-style:italic;font-size:12px;line-height:1.7;color:var(--text);">
« En soumettant ce formulaire, je consens librement à recevoir des communications électroniques de WEVAL Consulting relatives à des informations médicales, scientifiques et professionnelles en lien avec ma pratique. Conformément à la réglementation en vigueur, mes données seront traitées de manière confidentielle. Je peux exercer mes droits d'accès, de rectification et de suppression, et retirer mon consentement à tout moment via le lien de désinscription. »
</div>
<h4 style="margin-top:14px;">⚖️ Cadre Réglementaire Applicable</h4>
<ul style="font-size:12px;color:var(--text2);margin:8px 0 0 16px;line-height:2;">
<li><strong>Maroc</strong> — Loi 09-08 <span style="color:var(--teal);font-weight:700;">CNDP</span></li>
<li><strong>Tunisie</strong> — Loi 63-2004 <span style="color:var(--teal);font-weight:700;">INPDP</span></li>
<li><strong>Algérie</strong> — Loi 18-07 <span style="color:var(--teal);font-weight:700;">ANPDP</span></li>
</ul>
<div style="margin-top:10px;font-size:11px;color:var(--text3);">
<i class="fas fa-globe"></i> Module consentement : <a href="https://consent.wevup.app" target="_blank" style="color:var(--teal);">https://consent.wevup.app</a>
</div>
<h4 style="margin-top:14px;">🎯 Spécialités Ciblées</h4>
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:8px;">
<span class="badge b-optin">Cardiologie</span>
<span class="badge b-optin">Endocrinologie</span>
<span class="badge b-optin">Gastro-entérologie</span>
<span class="badge b-optin">Neurologie</span>
<span class="badge b-optin">Oncologie</span>
<span class="badge b-optin">Pneumologie</span>
<span class="badge b-optin">Rhumatologie</span>
<span class="badge b-optin">Dermatologie</span>
<span class="badge b-optin">Ophtalmologie</span>
<span class="badge b-optin">Médecine Interne</span>
<span class="badge b-pending" style="opacity:.7;">Gynécologie (à venir)</span>
<span class="badge b-pending" style="opacity:.7;">Cardiologie interventionnelle (à venir)</span>
</div>
</div>
<div class="consent-box">
<h4>🔄 Workflow Consentement en 3 niveaux</h4>
<p><span class="badge b-pending">PENDING</span> → Premier email envoyé avec lien opt-in + opt-out<br>
<span class="badge b-implicit" style="margin-top:4px;">IMPLICIT</span> → Le médecin a ouvert l'email (soft opt-in, intérêt légitime)<br>
<span class="badge b-optin" style="margin-top:4px;">OPT-IN</span> → Le médecin a cliqué "Je souhaite recevoir" (explicit consent)<br>
<span class="badge b-optout" style="margin-top:4px;">OPT-OUT</span> → Le médecin a cliqué "Se désinscrire" (respect immédiat)</p>
</div>
<div class="btn-row">
<button class="btn btn-teal" onclick="applyImplicitConsent()"><i class="fas fa-check-double"></i> Appliquer Implicit aux Opens</button>
<button class="btn btn-ghost" onclick="exportConsent('opt-in')"><i class="fas fa-download"></i> Export Opt-in</button>
<button class="btn btn-ghost" onclick="exportConsent('opt-out')"><i class="fas fa-download"></i> Export Opt-out</button>
</div>
</div>
</div>
<div class="card">
<div class="card-head"><h3><i class="fas fa-cog"></i> Outils Consentement</h3></div>
<div class="card-body">
<div style="margin-bottom:12px;">
<label style="font-size:11px;font-weight:600;color:var(--text2);">Changer statut par lot</label>
<div class="toolbar" style="margin-top:6px;">
<select id="con-from"><option value="pending">De: Pending</option><option value="implicit">De: Implicit</option></select>
<span style="color:var(--text3);"></span>
<select id="con-to"><option value="implicit">Vers: Implicit</option><option value="opt-in">Vers: Opt-in</option><option value="opt-out">Vers: Opt-out</option></select>
<select id="con-pays"><option value="">Tous pays</option><option value="MA">MA</option><option value="TN">TN</option><option value="DZ">DZ</option></select>
<button class="btn btn-sm btn-teal" onclick="batchConsent()"><i class="fas fa-exchange-alt"></i> Appliquer</button>
</div>
</div>
<div style="margin-top:16px;">
<h4 style="font-size:12px;font-weight:700;margin-bottom:6px;">URL Pages Consentement</h4>
<div style="font-size:11px;color:var(--text2);background:var(--bg);padding:10px;border-radius:6px;">
<p>📥 <strong>Opt-in:</strong> <code>https://consent.wevup.app/ethica-consent.php?action=optin&token={TOKEN}</code></p>
<p style="margin-top:4px;">📤 <strong>Opt-out:</strong> <code>https://consent.wevup.app/ethica-consent.php?action=optout&token={TOKEN}</code></p>
<p style="margin-top:4px;">📋 <strong>Preferences:</strong> <code>https://consent.wevup.app/ethica-consent.php?action=prefs&token={TOKEN}</code></p>
</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-head"><h3><i class="fas fa-list"></i> Contacts par Statut Consentement</h3>
<div class="toolbar" style="margin:0;">
<select id="cl-filter" onchange="loadConsentList()">
<option value="">Tous</option><option value="pending">Pending</option><option value="implicit">Implicit</option><option value="opt-in">Opt-in</option><option value="opt-out">Opt-out</option>
</select>
</div>
</div>
<div class="card-body" style="overflow-x:auto;" id="consent-list">Chargement...</div>
<div class="pager" id="consent-pager"></div>
</div>
</div>
<!-- ============ SCRAPING MULTI-SOURCES ============ -->
<div class="panel" id="scraping">
<div class="stats-row">
<div class="stat"><div class="stat-lbl">Total HCP</div><div class="stat-val" id="ss-total"><?=number_format($total)?></div></div>
<div class="stat blue"><div class="stat-lbl">Sources</div><div class="stat-val" id="ss-sources">30</div></div>
<div class="stat green"><div class="stat-lbl">Aujourd'hui</div><div class="stat-val" id="ss-today">0</div></div>
<div class="stat orange"><div class="stat-lbl">Scraper</div><div class="stat-val" id="ss-running"><?=$running_scr?'🟠 Actif':'🟢 Idle'?></div></div>
</div>
<div class="card">
<div class="card-head">
<h3><i class="fas fa-spider"></i> Sources de Scraping</h3>
<div>
<button class="btn btn-sm btn-ghost" onclick="toggleAllSources(true)"><i class="fas fa-check-double"></i> Tout cocher</button>
<button class="btn btn-sm btn-ghost" onclick="toggleAllSources(false)"><i class="fas fa-times"></i> Tout décocher</button>
</div>
</div>
<div class="card-body">
<div class="src-grid" id="sources-grid">Chargement...</div>
</div>
</div>
<div class="grid-2">
<div class="card">
<div class="card-head"><h3><i class="fas fa-play-circle"></i> Lancer Scraping</h3></div>
<div class="card-body">
<div class="toolbar">
<select id="scr-pays"><option value="all">Tous pays</option><option value="MA">🇲🇦 Maroc</option><option value="TN">🇹🇳 Tunisie</option><option value="DZ">🇩🇿 Algérie</option></select>
<select id="scr-spec"><option value="all">Toutes spécialités</option></select>
<select id="scr-mode">
<option value="all">Tous modes</option>
<option value="google">Google Search</option>
<option value="directories">Annuaires</option>
<option value="generate">Génération</option>
</select>
</div>
<button class="btn btn-teal" onclick="launchScraping()"><i class="fas fa-rocket"></i> Lancer Scraping</button>
<button class="btn btn-ghost" onclick="launchScrapingContinuous()" style="margin-left:6px;"><i class="fas fa-infinity"></i> Continu (24h)</button>
<div id="scr-status" style="margin-top:10px;font-size:12px;color:var(--text2);"></div>
</div>
</div>
<div class="card">
<div class="card-head"><h3><i class="fas fa-terminal"></i> Logs Live</h3></div>
<div class="card-body" style="max-height:300px;overflow-y:auto;">
<pre id="scr-logs" style="font-size:10px;color:var(--teal);background:var(--bg);padding:10px;border-radius:6px;white-space:pre-wrap;max-height:260px;overflow-y:auto;">En attente...</pre>
</div>
</div>
</div>
<div class="card">
<div class="card-head"><h3><i class="fas fa-history"></i> Historique Scraping</h3></div>
<div class="card-body" style="overflow-x:auto;" id="scr-history">Chargement...</div>
</div>
</div>
</div>
<script>
const API = '/api/ethica-api.php';
const DRILL = '/api/ethica-drill.php';
const FLAGS = {MA:'🇲🇦',TN:'🇹🇳',DZ:'🇩🇿',ALL:'🌍'};
const VAL_BADGES = {valid:'b-valid',invalid:'b-invalid',risky:'b-risky',unknown:'b-unknown',catch_all:'b-catch'};
const CON_BADGES = {pending:'b-pending','opt-in':'b-optin','opt-out':'b-optout',implicit:'b-implicit',unsubscribed:'b-optout'};
function showTab(id) {
document.querySelectorAll('.panel').forEach(p=>p.classList.remove('active'));
document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
document.getElementById(id).classList.add('active');
event.target.closest('.tab').classList.add('active');
if(id==='validator') loadValidator();
if(id==='consent') loadConsent();
if(id==='scraping') loadScraping();
}
function toast(m){const t=document.getElementById('toast');t.textContent=m;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),3000);}
// ========== VALIDATOR ==========
let valPage = 1;
async function loadValidator() {
const d = await (await fetch(DRILL+'?action=summary')).json();
// Fetch validation stats
const r = await (await fetch(API+'?action=validation_stats')).json();
if(r.stats) {
Object.entries(r.stats).forEach(([k,v]) => { const el=document.getElementById('sv-'+k); if(el) el.textContent=v.toLocaleString(); });
document.getElementById('sv-checked').textContent = ((r.stats.valid||0)+(r.stats.invalid||0)+(r.stats.risky||0)+(r.stats.catch_all||0)).toLocaleString();
// Bars
const total = Object.values(r.stats).reduce((a,b)=>a+b,0);
const colors = {valid:'#22c55e',invalid:'#ef4444',risky:'#f59e0b',catch_all:'#06b6d4',unknown:'#64748b'};
let barsHtml = '';
Object.entries(r.stats).forEach(([k,v]) => {
const pct = total>0?(v/total*100):0;
barsHtml += `<div class="bar-h"><span class="bar-lbl">${k}</span><div class="bar-track"><div class="bar-fill" style="width:${pct}%;background:${colors[k]||'#64748b'}"></div></div><span class="bar-cnt">${v.toLocaleString()} (${pct.toFixed(1)}%)</span></div>`;
});
document.getElementById('val-bars').innerHTML = barsHtml;
}
// History
if(r.history) {
let h = '<table class="tbl"><thead><tr><th>Batch</th><th>Total</th><th>Valid</th><th>Invalid</th><th>Unknown</th><th>Durée</th><th>Date</th></tr></thead><tbody>';
r.history.forEach(b => {
h += `<tr><td style="font-size:10px;">${b.batch_id}</td><td>${b.total_checked}</td><td style="color:var(--green)">${b.valid}</td><td style="color:var(--red)">${b.invalid}</td><td>${b.unknown}</td><td>${b.duration_sec}s</td><td style="font-size:10px;">${new Date(b.started_at).toLocaleString('fr-FR')}</td></tr>`;
});
h += '</tbody></table>';
document.getElementById('val-history').innerHTML = h;
}
loadValList();
}
async function loadValList(page) {
if(page) valPage=page;
const filter = document.getElementById('vl-filter').value;
const search = document.getElementById('vl-search').value;
let url = API+`?action=validation_list&page=${valPage}&limit=50`;
if(filter) url += `&filter=${filter}`;
if(search) url += `&search=${encodeURIComponent(search)}`;
const r = await (await fetch(url)).json();
let h = '<table class="tbl"><thead><tr><th>Email</th><th>Status</th><th>MX</th><th>SMTP</th><th>Spécialité</th><th>Pays</th><th>Téléphone</th><th>Adresse</th><th>Vérifié</th></tr></thead><tbody>';
(r.contacts||[]).forEach(c => {
h += `<tr><td style="font-weight:500;font-size:10px;">${c.email}</td>
<td><span class="badge ${VAL_BADGES[c.email_valid]||'b-unknown'}">${c.email_valid}</span></td>
<td style="font-size:10px;color:var(--text3)">${c.mx_host||''}</td>
<td>${c.smtp_code||''}</td>
<td style="font-size:9px;color:var(--text3);max-width:200px;overflow:hidden;text-overflow:ellipsis;">${c.smtp_msg||''}</td>
<td><span class="badge b-pending">${c.specialite}</span></td>
<td>${FLAGS[c.pays]||''} ${c.pays}</td>
<td style="font-size:11px;">${c.telephone||''}</td>
<td style="font-size:10px;max-width:130px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${c.adresse||''}</td>
<td style="font-size:9px;">${c.email_checked_at?new Date(c.email_checked_at).toLocaleString('fr-FR'):''}</td></tr>`;
});
h += '</tbody></table>';
document.getElementById('val-list').innerHTML = h;
// Pager
let pg = '';
const pages = r.pages||1;
if(valPage>1) pg += `<button class="btn btn-sm btn-ghost" onclick="loadValList(${valPage-1})"></button>`;
for(let i=Math.max(1,valPage-3);i<=Math.min(pages,valPage+3);i++) pg+=`<button class="btn btn-sm ${i===valPage?'btn-teal active':'btn-ghost'}" onclick="loadValList(${i})">${i}</button>`;
if(valPage<pages) pg+=`<button class="btn btn-sm btn-ghost" onclick="loadValList(${valPage+1})"></button>`;
pg += `<span class="pager-info">${(r.total||0).toLocaleString()} résultats</span>`;
document.getElementById('val-pager').innerHTML = pg;
}
async function launchValidation(continuous) {
const pays = document.getElementById('vf-pays').value;
const batch = document.getElementById('vf-batch').value;
const fd = new FormData();
fd.append('action','launch_validation');
fd.append('batch', batch);
fd.append('pays', pays);
fd.append('continuous', continuous?'1':'0');
document.getElementById('btn-validate').disabled = true;
document.getElementById('val-progress').innerHTML = '<span style="color:var(--orange);">⏳ Validation en cours...</span>';
const r = await (await fetch(API, {method:'POST', body:fd})).json();
toast(r.message||'Validation lancée');
document.getElementById('btn-validate').disabled = false;
// Poll progress
pollValidation();
}
async function pollValidation() {
for(let i=0;i<30;i++) {
await new Promise(r=>setTimeout(r,3000));
const r = await (await fetch(API+'?action=validation_stats')).json();
if(r.stats) {
const checked = (r.stats.valid||0)+(r.stats.invalid||0)+(r.stats.risky||0)+(r.stats.catch_all||0);
document.getElementById('val-progress').innerHTML = `<span style="color:var(--teal);">✅ ${checked.toLocaleString()} vérifiés | V:${r.stats.valid||0} I:${r.stats.invalid||0} R:${r.stats.risky||0}</span>`;
Object.entries(r.stats).forEach(([k,v])=>{const el=document.getElementById('sv-'+k);if(el)el.textContent=v.toLocaleString();});
}
if(!r.running) break;
}
loadValidator();
}
// ========== CONSENT ==========
let conPage = 1;
async function loadConsent() {
const r = await (await fetch(API+'?action=consent_stats')).json();
if(r.stats) Object.entries(r.stats).forEach(([k,v])=>{const el=document.getElementById('sc-'+k.replace('-',''));if(el)el.textContent=v.toLocaleString();});
loadConsentList();
}
async function loadConsentList(page) {
if(page) conPage=page;
const filter = document.getElementById('cl-filter').value;
let url = API+`?action=consent_list&page=${conPage}&limit=50`;
if(filter) url += `&filter=${filter}`;
const r = await (await fetch(url)).json();
let h = '<table class="tbl"><thead><tr><th>Email</th><th>Nom</th><th>Consent</th><th>Email Valid</th><th>Téléphone</th><th>Adresse</th><th>Téléphone</th><th>Adresse</th><th>Méthode</th><th>Spécialité</th><th>Pays</th></tr></thead><tbody>';
(r.contacts||[]).forEach(c => {
h += `<tr><td style="font-weight:500;font-size:10px;">${c.email}</td>
<td>${c.nom||''} ${c.prenom||''}</td>
<td><span class="badge ${CON_BADGES[c.consent_status]||'b-pending'}">${c.consent_status}</span></td>
<td><span class="badge ${VAL_BADGES[c.email_valid]||'b-unknown'}">${c.email_valid||'unknown'}</span></td>
<td style="font-size:11px;">${c.telephone||''}</td><td style="font-size:10px;max-width:130px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${c.adresse||''}</td><td style="font-size:10px;">${c.consent_method||''}</td>
<td style="font-size:9px;">${c.consent_date?new Date(c.consent_date).toLocaleString('fr-FR'):''}</td>
<td><span class="badge b-pending">${c.specialite}</span></td>
<td>${FLAGS[c.pays]||''}</td></tr>`;
});
h += '</tbody></table>';
document.getElementById('consent-list').innerHTML = h;
let pg='';const pages=r.pages||1;
for(let i=Math.max(1,conPage-3);i<=Math.min(pages,conPage+3);i++) pg+=`<button class="btn btn-sm ${i===conPage?'btn-teal active':'btn-ghost'}" onclick="loadConsentList(${i})">${i}</button>`;
pg+=`<span class="pager-info">${(r.total||0).toLocaleString()}</span>`;
document.getElementById('consent-pager').innerHTML=pg;
}
async function applyImplicitConsent() {
const fd = new FormData(); fd.append('action','apply_implicit_consent');
const r = await (await fetch(API,{method:'POST',body:fd})).json();
toast(`${r.updated||0} contacts → implicit consent`);
loadConsent();
}
async function batchConsent() {
const fd = new FormData();
fd.append('action','batch_consent');
fd.append('from', document.getElementById('con-from').value);
fd.append('to', document.getElementById('con-to').value);
fd.append('pays', document.getElementById('con-pays').value);
const r = await (await fetch(API,{method:'POST',body:fd})).json();
toast(`${r.updated||0} contacts mis à jour`);
loadConsent();
}
function exportConsent(status) { window.open(DRILL+`?action=export_csv&consent=${status}`, '_blank'); }
// ========== SCRAPING ==========
async function loadScraping() {
const r = await (await fetch(API+'?action=scraping_sources')).json();
let grid = '';
const types = {};
(r.sources||[]).forEach(s => {
if(!types[s.type]) types[s.type] = [];
types[s.type].push(s);
});
Object.entries(types).forEach(([type, sources]) => {
grid += `<div style="grid-column:1/-1;font-size:11px;font-weight:700;color:var(--teal);margin-top:8px;text-transform:uppercase;">${type} (${sources.length})</div>`;
sources.forEach(s => {
grid += `<label class="src-item">
<input type="checkbox" checked data-src-id="${s.id}" data-src-name="${s.name}">
<span class="src-name">${s.name}</span>
<span class="src-pays">${FLAGS[s.pays]||s.pays}</span>
<span class="src-type">${s.type}</span>
</label>`;
});
});
document.getElementById('sources-grid').innerHTML = grid;
// Load specs dropdown
const specs = await (await fetch(API+'?action=specialites')).json();
let opts = '<option value="all">Toutes spécialités</option>';
(specs.specialites||[]).forEach(s => opts += `<option value="${s.code}">${s.label}</option>`);
document.getElementById('scr-spec').innerHTML = opts;
// Scraping history
const h = await (await fetch(API+'?action=scraping_status')).json();
let hist = '<table class="tbl"><thead><tr><th>Date</th><th>Spécialité</th><th>Pays</th><th>Trouvés</th><th>Nouveaux</th><th>Durée</th></tr></thead><tbody>';
(h.recent_logs||[]).forEach(l => {
hist += `<tr><td style="font-size:10px;">${new Date(l.created_at).toLocaleString('fr-FR')}</td><td>${l.specialite||'all'}</td><td>${l.pays||'all'}</td><td>${l.emails_found}</td><td style="color:var(--green);font-weight:700;">${l.emails_new}</td><td>${l.duration_sec}s</td></tr>`;
});
hist += '</tbody></table>';
document.getElementById('scr-history').innerHTML = hist;
// Logs
refreshLogs();
}
function toggleAllSources(checked) {
document.querySelectorAll('#sources-grid input[type=checkbox]').forEach(cb => cb.checked = checked);
}
async function launchScraping() {
const sources = [...document.querySelectorAll('#sources-grid input:checked')].map(cb=>cb.dataset.srcName);
const pays = document.getElementById('scr-pays').value;
const spec = document.getElementById('scr-spec').value;
const mode = document.getElementById('scr-mode').value;
const fd = new FormData();
fd.append('action','launch_scraping');
fd.append('sources', JSON.stringify(sources));
fd.append('pays', pays);
fd.append('spec', spec);
fd.append('mode', mode);
const r = await (await fetch(API,{method:'POST',body:fd})).json();
toast(r.message||'Scraping lancé');
document.getElementById('scr-status').innerHTML = '<span style="color:var(--orange);">⏳ Scraping en cours...</span>';
pollLogs();
}
async function launchScrapingContinuous() {
const fd = new FormData();
fd.append('action','launch_scraping_continuous');
const r = await (await fetch(API,{method:'POST',body:fd})).json();
toast(r.message||'Scraping continu lancé (24h)');
}
async function refreshLogs() {
const r = await (await fetch(API+'?action=scraping_logs')).json();
document.getElementById('scr-logs').textContent = r.logs || 'Pas de logs récents';
}
async function pollLogs() {
for(let i=0;i<20;i++) { await new Promise(r=>setTimeout(r,5000)); refreshLogs(); }
}
// Init
loadValidator();
// Populate specs for scraping when tab opens
setInterval(async()=>{
const r = await (await fetch(API+'?action=validation_stats')).json();
if(r.stats) Object.entries(r.stats).forEach(([k,v])=>{const el=document.getElementById('sv-'+k);if(el)el.textContent=v.toLocaleString();});
}, 15000);
</script>
</body>
</html>