Files
html/ethica-pipeline.html
Opus 1bc0f9f3ee
Some checks failed
WEVAL NonReg / nonreg (push) Has been cancelled
phase36 dashboards alertes UX doctrine 60 - 42 pages total session
Dashboards avec champs alertes enrichis:
- agent-social-feed (1 empty field)
- ethica-pipeline (5 empty fields)
- web-ia-status (reste pour prochain - all_failed temporaire)

Total pages UX doctrine 60: 42 (40 + 2 aujourd hui)

Scan doublons identifies:
- 15 hubs avec variantes -v2/-v3/-NEW/-OLD (candidats consolidation)
- 74 position:fixed declarations top+right sur 40 pages (analyse overlap pending)

Autre Claude wave 311 actif (WEVIA Master pivot + KB-augment + sovereign fallback).
Server load 99, disk 93% - fragile. Actions suivantes deleguees a cron auto + autre Claude.
2026-04-24 12:27:07 +02:00

562 lines
31 KiB
HTML

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ethica — Pipeline E2E & Performance</title>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{
--bg:#0a0a12;--card:#12121e;--border:#1e1e35;--text:#e2e8f0;--muted:#64748b;
--purple:#7c3aed;--purple-glow:rgba(124,58,237,0.3);
--green:#22c55e;--green-glow:rgba(34,197,94,0.2);
--amber:#f59e0b;--red:#ef4444;--blue:#3b82f6;--cyan:#06b6d4;
--teal:#14b8a6;--pink:#ec4899;
}
body{font-family:'DM Sans',sans-serif;background:var(--bg);color:var(--text);min-height:100vh;overflow-x:hidden}
.topbar{background:linear-gradient(135deg,#12121e 0%,#1a1a2e 100%);border-bottom:1px solid var(--border);padding:14px 24px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:100;backdrop-filter:blur(20px)}
.topbar h1{font-size:18px;font-weight:700;display:flex;align-items:center;gap:10px}
.topbar h1 span{background:var(--purple);padding:3px 10px;border-radius:6px;font-size:11px;font-weight:600;letter-spacing:0.5px}
.live-dot{width:8px;height:8px;border-radius:50%;background:#22c55e;animation:pulse 2s infinite;display:inline-block;margin-right:6px}
@keyframes pulse{0%,100%{opacity:1;box-shadow:0 0 0 0 rgba(34,197,94,0.4)}50%{opacity:0.7;box-shadow:0 0 0 6px rgba(34,197,94,0)}}
.container{max-width:1400px;margin:0 auto;padding:20px 24px}
/* PIPELINE SECTION */
.pipeline-section{margin-bottom:32px}
.section-title{font-size:14px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:1.5px;margin-bottom:16px}
.pipeline{display:flex;gap:0;align-items:stretch;overflow-x:auto;padding:8px 0}
.pipe-stage{flex:1;min-width:160px;position:relative;padding:20px 16px;background:var(--card);border:1px solid var(--border);text-align:center;transition:all 0.3s}
.pipe-stage:first-child{border-radius:12px 0 0 12px}
.pipe-stage:last-child{border-radius:0 12px 12px 0}
.pipe-stage:hover{background:#1a1a30;border-color:var(--purple);z-index:2;transform:translateY(-2px)}
.pipe-stage .stage-icon{font-size:28px;margin-bottom:8px}
.pipe-stage .stage-name{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:var(--muted);margin-bottom:6px}
.pipe-stage .stage-value{font-size:26px;font-weight:800;font-family:'JetBrains Mono',monospace}
.pipe-stage .stage-sub{font-size:11px;color:var(--muted);margin-top:4px}
.pipe-arrow{display:flex;align-items:center;color:var(--purple);font-size:20px;padding:0 2px;animation:flowRight 1.5s infinite}
@keyframes flowRight{0%,100%{opacity:0.3;transform:translateX(0)}50%{opacity:1;transform:translateX(3px)}}
.pipe-stage.active{border-color:var(--green);box-shadow:0 0 20px var(--green-glow)}
.pipe-stage.active::after{content:'';position:absolute;top:-1px;left:0;right:0;height:3px;background:var(--green);border-radius:3px 3px 0 0}
/* FLOW PARTICLES */
.flow-track{position:relative;height:40px;margin:0 -24px 24px;overflow:hidden;background:linear-gradient(90deg,transparent,rgba(124,58,237,0.05),transparent)}
.flow-particle{position:absolute;width:6px;height:6px;border-radius:50%;background:var(--purple);top:50%;transform:translateY(-50%);animation:flowParticle 3s linear infinite;opacity:0.6}
@keyframes flowParticle{0%{left:-2%;opacity:0}10%{opacity:0.8}90%{opacity:0.8}100%{left:102%;opacity:0}}
/* KPI GRID */
.kpi-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:14px;margin-bottom:28px}
.kpi-card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:18px 20px;position:relative;overflow:hidden;transition:all 0.3s}
.kpi-card:hover{border-color:var(--purple);transform:translateY(-1px)}
.kpi-card .kpi-label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:1px;color:var(--muted);margin-bottom:8px}
.kpi-card .kpi-value{font-size:28px;font-weight:800;font-family:'JetBrains Mono',monospace;line-height:1}
.kpi-card .kpi-sub{font-size:12px;color:var(--muted);margin-top:6px}
.kpi-card .kpi-bar{position:absolute;bottom:0;left:0;height:3px;border-radius:0 3px 0 0;transition:width 1.5s ease}
.kpi-card.purple .kpi-value{color:var(--purple)}.kpi-card.purple .kpi-bar{background:var(--purple)}
.kpi-card.green .kpi-value{color:var(--green)}.kpi-card.green .kpi-bar{background:var(--green)}
.kpi-card.amber .kpi-value{color:var(--amber)}.kpi-card.amber .kpi-bar{background:var(--amber)}
.kpi-card.blue .kpi-value{color:var(--blue)}.kpi-card.blue .kpi-bar{background:var(--blue)}
.kpi-card.cyan .kpi-value{color:var(--cyan)}.kpi-card.cyan .kpi-bar{background:var(--cyan)}
.kpi-card.red .kpi-value{color:var(--red)}.kpi-card.red .kpi-bar{background:var(--red)}
.kpi-card.teal .kpi-value{color:var(--teal)}.kpi-card.teal .kpi-bar{background:var(--teal)}
.kpi-card.pink .kpi-value{color:var(--pink)}.kpi-card.pink .kpi-bar{background:var(--pink)}
/* REACH TABLE */
.two-col{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:28px}
@media(max-width:900px){.two-col{grid-template-columns:1fr}}
.panel{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:20px;overflow:hidden}
.panel h3{font-size:14px;font-weight:700;margin-bottom:14px;display:flex;align-items:center;gap:8px}
.reach-table{width:100%;border-collapse:collapse}
.reach-table th{text-align:left;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:1px;color:var(--muted);padding:8px 10px;border-bottom:1px solid var(--border)}
.reach-table td{padding:8px 10px;font-size:13px;border-bottom:1px solid rgba(255,255,255,0.03)}
.reach-table tr:hover{background:rgba(124,58,237,0.05)}
.reach-table .spec-name{font-weight:600}
.reach-bar{height:6px;border-radius:3px;background:rgba(124,58,237,0.15);overflow:hidden;min-width:80px}
.reach-bar-fill{height:100%;border-radius:3px;background:linear-gradient(90deg,var(--purple),var(--cyan));transition:width 1.5s ease}
.country-badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:10px;font-weight:700;letter-spacing:0.5px}
.country-badge.dz{background:rgba(34,197,94,0.15);color:#22c55e}
.country-badge.ma{background:rgba(239,68,68,0.15);color:#ef4444}
.country-badge.tn{background:rgba(59,130,246,0.15);color:#3b82f6}
/* LIVE FEED */
.live-feed{max-height:320px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--purple) var(--card)}
.feed-item{display:flex;align-items:center;gap:10px;padding:8px 12px;border-radius:8px;margin-bottom:4px;animation:fadeSlideIn 0.4s ease;font-size:12px}
.feed-item:hover{background:rgba(255,255,255,0.02)}
@keyframes fadeSlideIn{from{opacity:0;transform:translateX(-10px)}to{opacity:1;transform:translateX(0)}}
.feed-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
.feed-time{color:var(--muted);font-family:'JetBrains Mono',monospace;font-size:10px;min-width:50px}
/* ANIMATED COUNTERS */
.counter{display:inline-block;transition:all 0.3s}
/* PERFORMANCE GAUGES */
.gauge-row{display:flex;gap:20px;flex-wrap:wrap;margin-top:12px}
.gauge{flex:1;min-width:100px;text-align:center}
.gauge-circle{width:80px;height:80px;border-radius:50%;margin:0 auto 8px;position:relative;display:flex;align-items:center;justify-content:center}
.gauge-circle svg{position:absolute;top:0;left:0;transform:rotate(-90deg)}
.gauge-value{font-size:18px;font-weight:800;font-family:'JetBrains Mono',monospace}
.gauge-label{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:0.5px}
.back-btn{background:rgba(124,58,237,0.15);border:1px solid rgba(124,58,237,0.3);color:var(--purple);padding:6px 14px;border-radius:8px;cursor:pointer;font-size:12px;font-weight:600;text-decoration:none;transition:all 0.2s}
.back-btn:hover{background:var(--purple);color:#fff}
.refresh-btn{background:var(--green);border:none;color:#fff;padding:6px 14px;border-radius:8px;cursor:pointer;font-size:12px;font-weight:600;transition:all 0.2s}
.refresh-btn:hover{opacity:0.85}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
<!-- DOCTRINE-60-UX-ENRICH cerebras-qwen-235b 20260424-122240 --><style id="doctrine60-ux-ethica-pipeline">
body::before {
content: '';
position: fixed;
width: 100%;
height: 100%;
background: radial-gradient(circle, rgba(0,0,0,0.12), transparent 70%);
z-index: -1;
pointer-events: none;
}
.card, .btn, .kpi, .panel {
opacity: 0;
transform: translateY(20px);
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.enter-stagger {
opacity: 1;
transform: translateY(0);
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.pulse, .active, .live-indicator, .online {
animation: pulse 3s ease-in-out infinite;
}
.card:hover {
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
border-color: var(--accent);
}
.modal, .chat, .speech, .overlay {
backdrop-filter: blur(12px);
}
</style>
</head>
<body>
<div class="topbar">
<h1>
<svg width="28" height="28" viewBox="0 0 28 28"><circle cx="14" cy="14" r="13" fill="none" stroke="#7c3aed" stroke-width="2"/><path d="M8 14l4 4 8-8" stroke="#22c55e" stroke-width="2.5" fill="none" stroke-linecap="round"/></svg>
ETHICA <span>PIPELINE E2E</span>
</h1>
<div style="display:flex;gap:10px;align-items:center">
<span style="font-family:'JetBrains Mono';font-size:11px;color:var(--muted)" id="clock"></span>
<span class="live-dot"></span>
<button class="refresh-btn" onclick="loadData()">Refresh</button>
<a href="/ethica-monitor.html" class="back-btn">← Ethica</a>
</div>
</div>
<div class="container">
<div class="pipeline-section">
<div class="section-title">Pipeline de Collecte → Validation → Delivery</div>
<div class="pipeline" id="pipeline">
<div class="pipe-stage active"><div class="stage-icon">🔍</div><div class="stage-name">Scraping</div><div class="stage-value" id="p-scraping">12</div><div class="stage-sub">sources actives</div></div>
<div class="pipe-arrow"></div>
<div class="pipe-stage active"><div class="stage-icon">📥</div><div class="stage-name">Collecte</div><div class="stage-value" id="p-collecte">130 600</div><div class="stage-sub">HCPs bruts</div></div>
<div class="pipe-arrow"></div>
<div class="pipe-stage active"><div class="stage-icon">🧹</div><div class="stage-name">Nettoyage</div><div class="stage-value" id="p-clean">116 033</div><div class="stage-sub">noms uniques</div></div>
<div class="pipe-arrow"></div>
<div class="pipe-stage active"><div class="stage-icon"></div><div class="stage-name">Validation</div><div class="stage-value" id="p-valid">102 234</div><div class="stage-sub">noms clean</div></div>
<div class="pipe-arrow"></div>
<div class="pipe-stage active"><div class="stage-icon">📧</div><div class="stage-name">Enrichment</div><div class="stage-value" id="p-email">107 604</div><div class="stage-sub">avec email</div></div>
<div class="pipe-arrow"></div>
<div class="pipe-stage" style="opacity:0.5"><div class="stage-icon">📤</div><div class="stage-name">Delivery</div><div class="stage-value" id="p-delivery">0</div><div class="stage-sub"></div></div>
<div class="pipe-arrow"></div>
<div class="pipe-stage" style="opacity:0.5"><div class="stage-icon">📊</div><div class="stage-name">Engagement</div><div class="stage-value" id="p-engage">0</div><div class="stage-sub">en attente GO</div></div>
</div>
</div>
<div class="flow-track" id="flowTrack"></div>
<div class="section-title">KPIs Temps Réel</div>
<div class="kpi-grid">
<div class="kpi-card purple"><div class="kpi-label">Total HCPs</div><div class="kpi-value" id="kpi-total">130 600</div><div class="kpi-sub">DZ 91K · MA 19K · TN 17K</div><div class="kpi-bar" style="width:100%"></div></div>
<div class="kpi-card green"><div class="kpi-label">Taux Email</div><div class="kpi-value" id="kpi-email-rate">82.4%</div><div class="kpi-sub">107 604 / 130 600</div><div class="kpi-bar" style="width:82%"></div></div>
<div class="kpi-card teal"><div class="kpi-label">Taux Téléphone</div><div class="kpi-value" id="kpi-tel-rate">95.8%</div><div class="kpi-sub">125 089 / 130 600</div><div class="kpi-bar" style="width:96%"></div></div>
<div class="kpi-card amber"><div class="kpi-label">Taux d'ouverture</div><div class="kpi-value" id="kpi-open-rate">N/A</div><div class="kpi-sub">0 campagne envoyee</div><div class="kpi-bar" style="width:0%"></div></div>
<div class="kpi-card blue"><div class="kpi-label">Taux de clic</div><div class="kpi-value" id="kpi-click-rate">N/A</div><div class="kpi-sub">0 campagne envoyee</div><div class="kpi-bar" style="width:0%"></div></div>
<div class="kpi-card red"><div class="kpi-label">Taux de rebond</div><div class="kpi-value" id="kpi-bounce-rate">N/A</div><div class="kpi-sub">0 campagne envoyee</div><div class="kpi-bar" style="width:0%"></div></div>
<div class="kpi-card cyan"><div class="kpi-label">Campagnes Ethica</div><div class="kpi-value" id="kpi-opens">0</div><div class="kpi-sub">aucun envoi</div><div class="kpi-bar" style="width:0%"></div></div>
<div class="kpi-card pink"><div class="kpi-label">Consentements</div><div class="kpi-value" id="kpi-clicks">0</div><div class="kpi-sub">en attente pilot</div><div class="kpi-bar" style="width:0%"></div></div>
</div>
<div class="two-col">
<div class="panel">
<h3>📊 Reach par Spécialité</h3>
<table class="reach-table">
<thead><tr><th>Spécialité</th><th>HCPs</th><th>Email</th><th>Reach</th></tr></thead>
<tbody id="specTable"></tbody>
</table>
</div>
<div class="panel">
<h3>⚡ Performance Delivery</h3>
<div class="gauge-row" id="gauges">
<div class="gauge">
<div class="gauge-circle">
<svg width="80" height="80"><circle cx="40" cy="40" r="34" fill="none" stroke="rgba(255,255,255,0.05)" stroke-width="6"/><circle cx="40" cy="40" r="34" fill="none" stroke="#22c55e" stroke-width="6" stroke-dasharray="214" stroke-dashoffset="43" stroke-linecap="round"/></svg>
<span class="gauge-value" style="color:#22c55e">82%</span>
</div>
<div class="gauge-label">Email Coverage</div>
</div>
<div class="gauge">
<div class="gauge-circle">
<svg width="80" height="80"><circle cx="40" cy="40" r="34" fill="none" stroke="rgba(255,255,255,0.05)" stroke-width="6"/><circle cx="40" cy="40" r="34" fill="none" stroke="#3b82f6" stroke-width="6" stroke-dasharray="214" stroke-dashoffset="9" stroke-linecap="round"/></svg>
<span class="gauge-value" style="color:#3b82f6">96%</span>
</div>
<div class="gauge-label">Tel Coverage</div>
</div>
<div class="gauge">
<div class="gauge-circle">
<svg width="80" height="80"><circle cx="40" cy="40" r="34" fill="none" stroke="rgba(255,255,255,0.05)" stroke-width="6"/><circle cx="40" cy="40" r="34" fill="none" stroke="#f59e0b" stroke-width="6" stroke-dasharray="214" stroke-dashoffset="214" stroke-linecap="round"/></svg>
<span class="gauge-value" style="color:#f59e0b">0%</span>
</div>
<div class="gauge-label">Open Rate</div>
</div>
</div>
<h3 style="margin-top:20px">🔴 Live Feed</h3>
<div class="live-feed" id="liveFeed"></div>
</div>
</div>
<div class="section-title">Courbe de Progression</div>
<div style="display:grid;grid-template-columns:2fr 1fr;gap:20px;margin-bottom:28px">
<div class="panel" style="padding:20px">
<h3 style="font-size:14px;font-weight:700;margin-bottom:14px;display:flex;align-items:center;gap:8px">
<span style="color:var(--purple)">Croissance HCPs</span>
<span style="font-size:10px;background:rgba(124,58,237,0.15);color:var(--purple);padding:2px 8px;border-radius:4px">30 JOURS</span>
</h3>
<div style="position:relative;height:280px">
<canvas id="growthChart"></canvas>
</div>
</div>
<div class="panel" style="padding:20px">
<h3 style="font-size:14px;font-weight:700;margin-bottom:14px;color:var(--purple)">Delivery Funnel</h3>
<div style="position:relative;height:280px">
<canvas id="funnelChart"></canvas>
</div>
</div>
</div>
<div class="section-title">Couverture par Pays</div>
<div class="kpi-grid" style="grid-template-columns:repeat(3,1fr)">
<div class="kpi-card" style="border-left:3px solid #22c55e"><div class="kpi-label"><span class="country-badge dz">DZ</span> Algérie</div><div class="kpi-value" style="color:#22c55e">91 985</div><div class="kpi-sub">Email: 76 826 · Tél: 89 973</div><div class="kpi-bar green" style="width:70%"></div></div>
<div class="kpi-card" style="border-left:3px solid #ef4444"><div class="kpi-label"><span class="country-badge ma">MA</span> Maroc</div><div class="kpi-value" style="color:#ef4444">19 407</div><div class="kpi-sub">Email: 14 503 · Tél: 18 396</div><div class="kpi-bar red" style="width:15%"></div></div>
<div class="kpi-card" style="border-left:3px solid #3b82f6"><div class="kpi-label"><span class="country-badge tn">TN</span> Tunisie</div><div class="kpi-value" style="color:#3b82f6">17 329</div><div class="kpi-sub">Email: 14 394 · Tél: 16 720</div><div class="kpi-bar blue" style="width:13%"></div></div>
</div>
</div>
<script>
// Clock
setInterval(()=>{document.getElementById('clock').textContent=new Date().toLocaleTimeString('fr-FR')},1000);
document.getElementById('clock').textContent=new Date().toLocaleTimeString('fr-FR');
// Flow particles
const track=document.getElementById('flowTrack');
function spawnParticle(){
const p=document.createElement('div');
p.className='flow-particle';
p.style.animationDuration=(2+Math.random()*2)+'s';
p.style.animationDelay=(Math.random()*0.5)+'s';
p.style.top=(30+Math.random()*40)+'%';
p.style.width=p.style.height=(4+Math.random()*4)+'px';
p.style.background=['#7c3aed','#22c55e','#3b82f6','#f59e0b','#06b6d4'][Math.floor(Math.random()*5)];
track.appendChild(p);
setTimeout(()=>p.remove(),5000);
}
setInterval(spawnParticle,300);
// Specialty data (real)
const specs=[
{name:'Généraliste',total:12610,email:10388,pays:'DZ'},
{name:'Pédiatre',total:9254,email:7628,pays:'DZ'},
{name:'Dentiste',total:8138,email:6706,pays:'DZ'},
{name:'Gynécologue',total:7714,email:6357,pays:'DZ'},
{name:'Cardiologue',total:7555,email:6227,pays:'DZ+TN'},
{name:'Pharmacien',total:6919,email:5703,pays:'DZ'},
{name:'Médecin-interne',total:6714,email:5533,pays:'DZ'},
{name:'Gastro-entérologue',total:6408,email:5280,pays:'DZ'},
{name:'Allergologue',total:6339,email:5225,pays:'DZ'},
{name:'ORL',total:5505,email:4537,pays:'DZ'},
{name:'Orthopédiste',total:5200,email:4285,pays:'DZ'},
{name:'Pneumologue',total:4900,email:4039,pays:'DZ'},
{name:'Rhumatologue',total:4700,email:3873,pays:'DZ'},
{name:'Ophtalmologue',total:4200,email:3461,pays:'DZ+MA'},
{name:'Psychiatre',total:3800,email:3132,pays:'DZ'}
];
const maxSpec=specs[0].total;
const tbody=document.getElementById('specTable');
specs.forEach(s=>{
const reach=Math.round(s.email/s.total*100);
const barW=Math.round(s.total/maxSpec*100);
const tr=document.createElement('tr');
tr.innerHTML=`<td class="spec-name">${s.name}</td><td style="font-family:'JetBrains Mono';font-size:12px">${s.total.toLocaleString()}</td><td style="font-size:12px;color:var(--green)">${s.email.toLocaleString()}</td><td><div class="reach-bar"><div class="reach-bar-fill" style="width:${barW}%"></div></div><span style="font-size:10px;color:var(--muted);margin-left:6px">${reach}%</span></td>`;
tbody.appendChild(tr);
});
// Live feed simulation
const feed=document.getElementById('liveFeed');
let lastFeedTs='';
async function loadFeed(){
try{
const r=await fetch('/api/ethica-feed-api.php');
/* HTML_GUARD_V2_BATCH */ const _t_d=await r.text(); let d=null; {var _q=(_t_d||"").trim();if(_q.startsWith("<!DOCTYPE")||_q.startsWith("<html")){d={error:"[HTTP "+(r.status||"?")+"] Backend indisponible",isHtmlError:true};}else{try{d=JSON.parse(_q)}catch(e){d={error:"[JSON] "+e.message}}}}
if(!d.feed||!d.feed.length)return;
const newTs=d.feed[0].ts;
if(newTs===lastFeedTs)return;
lastFeedTs=newTs;
feed.innerHTML='';
d.feed.forEach(function(m){
var div=document.createElement('div');
div.className='feed-item';
div.innerHTML='<span class="feed-dot" style="background:'+m.color+'"></span><span class="feed-time">'+m.ts+'</span><span>'+m.icon+' '+m.msg+'</span>';
feed.appendChild(div);
});
}catch(e){console.error('Feed:',e)}
}
loadFeed();
setInterval(loadFeed,30000);
// Load real data from API
async function loadData(){
try{
const r=await fetch('/api/ethica-api.php?action=stats&token=ETHICA_API_2026_SECURE');
/* HTML_GUARD_V2_BATCH */ const _t_d=await r.text(); let d=null; {var _q=(_t_d||"").trim();if(_q.startsWith("<!DOCTYPE")||_q.startsWith("<html")){d={error:"[HTTP "+(r.status||"?")+"] Backend indisponible",isHtmlError:true};}else{try{d=JSON.parse(_q)}catch(e){d={error:"[JSON] "+e.message}}}}
if(d.total){
document.getElementById('kpi-total').textContent=d.total.toLocaleString();
document.getElementById('p-collecte').textContent=d.total.toLocaleString();
const emailRate=d.with_email&&d.total?Math.round(d.with_email/d.total*1000)/10:0;
const telRate=d.with_telephone&&d.total?Math.round(d.with_telephone/d.total*1000)/10:0;
document.getElementById('kpi-email-rate').textContent=emailRate+'%';
document.getElementById('kpi-tel-rate').textContent=telRate+'%';
document.getElementById('p-email').textContent=(d.with_email||0).toLocaleString();
}
}catch(e){console.error('API error:',e)}
}
loadData();
setInterval(loadData,60000);
// === PROGRESSION CHARTS ===
function initCharts() {
// Growth curve — cumulative
const rawData = [
{d:'01 Mar',v:0},{d:'05 Mar',v:2400},{d:'08 Mar',v:8900},{d:'10 Mar',v:15200},
{d:'12 Mar',v:28700},{d:'14 Mar',v:41300},{d:'16 Mar',v:43597},{d:'18 Mar',v:65000},
{d:'20 Mar',v:82400},{d:'22 Mar',v:105550},{d:'24 Mar',v:110308},{d:'25 Mar',v:111695},
{d:'26 Mar',v:112825},{d:'27 Mar',v:113588},{d:'28 Mar',v:114260},{d:'29 Mar',v:130600}
];
const ctx1 = document.getElementById('growthChart');
if (!ctx1) return;
new Chart(ctx1, {
type: 'line',
data: {
labels: rawData.map(d => d.d),
datasets: [{
label: 'HCPs Cumul\u00e9s',
data: rawData.map(d => d.v),
borderColor: '#7c3aed',
backgroundColor: function(context) {
const chart = context.chart;
const {ctx, chartArea} = chart;
if (!chartArea) return 'rgba(124,58,237,0.1)';
const gradient = ctx.createLinearGradient(0, chartArea.top, 0, chartArea.bottom);
gradient.addColorStop(0, 'rgba(124,58,237,0.3)');
gradient.addColorStop(0.5, 'rgba(124,58,237,0.08)');
gradient.addColorStop(1, 'rgba(124,58,237,0)');
return gradient;
},
fill: true,
tension: 0.4,
borderWidth: 2.5,
pointRadius: 3,
pointBackgroundColor: '#7c3aed',
pointBorderColor: '#0a0a12',
pointBorderWidth: 2,
pointHoverRadius: 6
},{
label: 'Emails Collect\u00e9s',
data: rawData.map(d => Math.round(d.v * 0.824)),
borderColor: '#22c55e',
backgroundColor: 'transparent',
fill: false,
tension: 0.4,
borderWidth: 1.5,
borderDash: [5, 3],
pointRadius: 0,
pointHoverRadius: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { labels: { color: '#94a3b8', font: { family: 'DM Sans', size: 11 }, boxWidth: 12, padding: 16 } },
tooltip: {
backgroundColor: '#1e1e35',
titleColor: '#e2e8f0',
bodyColor: '#94a3b8',
borderColor: '#7c3aed',
borderWidth: 1,
padding: 10,
titleFont: { family: 'JetBrains Mono', size: 12 },
bodyFont: { family: 'DM Sans', size: 12 },
callbacks: { label: ctx => ctx.dataset.label + ': ' + ctx.parsed.y.toLocaleString() }
}
},
scales: {
x: { grid: { color: 'rgba(255,255,255,0.04)' }, ticks: { color: '#64748b', font: { size: 10 } } },
y: {
grid: { color: 'rgba(255,255,255,0.04)' },
ticks: { color: '#64748b', font: { family: 'JetBrains Mono', size: 10 }, callback: v => v>=1000?(v/1000)+'K':v }
}
}
}
});
// Funnel / doughnut
const ctx2 = document.getElementById('funnelChart');
if (!ctx2) return;
new Chart(ctx2, {
type: 'doughnut',
data: {
labels: ['Emails (107K)', 'T\u00e9l sans email (17K)', 'Sans contact (6K)'],
datasets: [{
data: [107604, 17485, 5511],
backgroundColor: ['#7c3aed', '#06b6d4', '#1e1e35'],
borderColor: '#0a0a12',
borderWidth: 3,
hoverOffset: 8
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
cutout: '60%',
plugins: {
legend: {
position: 'bottom',
labels: { color: '#94a3b8', font: { family: 'DM Sans', size: 10 }, padding: 12, boxWidth: 10 }
},
tooltip: {
backgroundColor: '#1e1e35',
titleColor: '#e2e8f0',
bodyColor: '#94a3b8',
borderColor: '#7c3aed',
borderWidth: 1,
callbacks: { label: ctx => ctx.label + ': ' + ctx.parsed.toLocaleString() + ' (' + Math.round(ctx.parsed/1306) + '%)' }
}
}
}
});
}
// Init after DOM
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initCharts);
else setTimeout(initCharts, 500);
</script>
<!-- CARTO_REMOVED -->
<!-- === OPUS UNIVERSAL DRILL-DOWN v1 19avr — append-only, doctrine #14 === -->
<script>
(function(){
if (window.__opusUniversalDrill) return; window.__opusUniversalDrill = true;
var d = document;
var m = d.createElement('div');
m.id = 'opus-udrill';
m.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.82);backdrop-filter:blur(6px);display:none;align-items:center;justify-content:center;z-index:99995;padding:20px;cursor:pointer';
var inner = d.createElement('div');
inner.id = 'opus-udrill-in';
inner.style.cssText = 'max-width:900px;width:100%;max-height:90vh;overflow:auto;background:#0b0d15;border:1px solid rgba(99,102,241,0.35);border-radius:14px;padding:28px;cursor:default;box-shadow:0 20px 60px rgba(0,0,0,0.6);color:#e2e8f0;font:14px/1.55 Inter,system-ui,sans-serif';
inner.addEventListener('click', function(e){ e.stopPropagation(); });
m.appendChild(inner);
m.addEventListener('click', function(){ m.style.display='none'; });
d.addEventListener('keydown', function(e){ if(e.key==='Escape') m.style.display='none'; });
(d.body || d.documentElement).appendChild(m);
function openCard(card) {
// Clone card content + show close btn + increase font-size
var html = '<div style="display:flex;justify-content:flex-end;margin-bottom:14px"><button id="opus-udrill-close" style="padding:6px 14px;background:#171b2a;border:1px solid rgba(99,102,241,0.25);color:#e2e8f0;border-radius:8px;cursor:pointer;font-size:12px">✕ Fermer (Esc)</button></div>';
html += '<div style="transform-origin:top left;font-size:1.05em">' + card.outerHTML + '</div>';
inner.innerHTML = html;
d.getElementById('opus-udrill-close').onclick = function(){ m.style.display='none'; };
m.style.display = 'flex';
}
function wire(root) {
var sels = '.card,[class*="card"],.kpi,[class*="kpi"],.stat,[class*="stat"],.tile,[class*="tile"],.metric,[class*="metric"],.widget,[class*="widget"]';
var cards = root.querySelectorAll(sels);
for (var i = 0; i < cards.length; i++) {
var c = cards[i];
if (c.__opusWired) continue;
if (c.closest('button, a, input, select, textarea, #opus-udrill')) continue;
var r = c.getBoundingClientRect();
if (r.width < 60 || r.height < 40) continue;
c.__opusWired = true;
c.style.cursor = 'pointer';
c.setAttribute('role','button');
c.setAttribute('tabindex','0');
c.addEventListener('click', function(ev){
// If a more-specific drill is already active (e.g. pp-card custom), let it handle
if (ev.target.closest('[data-pp-id]') && window.__opusDrillInit) return;
if (ev.target.closest('a,button,input,select')) return;
ev.preventDefault(); ev.stopPropagation();
openCard(this);
});
c.addEventListener('keydown', function(ev){ if(ev.key==='Enter'||ev.key===' '){ev.preventDefault();openCard(this);} });
}
}
// Initial + mutation observer
var initRun = function(){ wire(d.body || d.documentElement); };
if (d.readyState === 'loading') d.addEventListener('DOMContentLoaded', initRun);
else initRun();
var mo = new MutationObserver(function(muts){
var newCard = false;
for (var i=0;i<muts.length;i++) if (muts[i].addedNodes.length) { newCard = true; break; }
if (newCard) initRun();
});
mo.observe(d.body || d.documentElement, {childList:true, subtree:true});
})();
</script>
<!-- === OPUS UNIVERSAL DRILL-DOWN END === -->
<script src="/api/a11y-auto-enhancer.js" defer></script>
<!-- WTP_UDOCK_V1 (Opus 21-avr t33b5) --><script src="/wtp-unified-dock.js" defer></script>
<script src="/opus-antioverlap-doctrine.js?v=1776776094" defer></script>
<!-- DOCTRINE-60-UX-JS --><script id="doctrine60-ux-js-ethica-pipeline">
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry, index) => {
if (entry.isIntersecting) {
setTimeout(() => {
entry.target.classList.add('enter-stagger');
}, index * 80);
}
});
}, { threshold: 0.1 });
document.querySelectorAll('.card, .btn, .kpi, .panel').forEach(el => observer.observe(el));
</script>
</body>
</html>