Files
html/world-map-live.html
2026-04-19 21:20:03 +02:00

250 lines
13 KiB
HTML
Raw 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.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>World Map Live Email Flux</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#0a0e17;color:#e0e6f0;font-family:'Segoe UI',system-ui,sans-serif;overflow:hidden;height:100vh}
.header{display:flex;align-items:center;justify-content:space-between;padding:10px 20px;background:#0d1220;border-bottom:1px solid #1a2340;z-index:10;position:relative}
.header h1{font-size:18px;font-weight:600;color:#d4a843}
.header .sub{font-size:12px;color:#6b7a99;margin-left:12px}
.clock{font-size:22px;font-weight:300;color:#8fa4c8;font-variant-numeric:tabular-nums}
.main{display:grid;grid-template-columns:300px 1fr 340px;height:calc(100vh - 48px - 80px)}
.panel{padding:16px;overflow-y:auto}
.panel-left{background:#0d1220;border-right:1px solid #1a2340}
.panel-right{background:#0d1220;border-left:1px solid #1a2340}
.panel h3{font-size:13px;font-weight:600;color:#8fa4c8;text-transform:uppercase;letter-spacing:1px;margin-bottom:12px}
#map{width:100%;height:100%;background:#0a0e17}
.country-row{display:flex;align-items:center;padding:6px 0;border-bottom:1px solid #111a2e}
.country-flag{width:22px;margin-right:8px;font-size:16px}
.country-name{flex:1;font-size:13px;color:#c0c8d8}
.country-cnt{font-size:13px;font-weight:600;color:#d4a843;min-width:70px;text-align:right}
.country-bar{width:80px;height:6px;background:#111a2e;border-radius:3px;margin-left:8px;overflow:hidden}
.country-bar-fill{height:100%;border-radius:3px;background:linear-gradient(90deg,#1e88e5,#42a5f5)}
.feed-item{padding:6px 0;border-bottom:1px solid #111a2e;font-size:12px;font-variant-numeric:tabular-nums}
.feed-time{color:#5a6a88;margin-right:6px}
.feed-type{padding:1px 6px;border-radius:3px;font-size:10px;font-weight:700;margin:0 4px}
.feed-open{background:#1b5e20;color:#66bb6a}
.feed-click{background:#e65100;color:#ff9800}
.feed-send{background:#1565c0;color:#42a5f5}
.feed-bounce{background:#b71c1c;color:#ef5350}
.feed-tid{color:#6b7a99;font-family:monospace;font-size:10px}
.bottom-bar{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:1px;background:#0d1220;border-top:1px solid #1a2340;height:80px}
.kpi{display:flex;flex-direction:column;align-items:center;justify-content:center;background:#0a0e17}
.kpi-val{font-size:24px;font-weight:700;color:#d4a843;font-variant-numeric:tabular-nums}
.kpi-val.green{color:#66bb6a}
.kpi-val.red{color:#ef5350}
.kpi-val.blue{color:#42a5f5}
.kpi-label{font-size:9px;color:#5a6a88;text-transform:uppercase;letter-spacing:1px;margin-top:2px}
.dot{display:inline-block;width:6px;height:6px;border-radius:50%;margin-right:4px}
.dot-live{background:#66bb6a;animation:pulse 2s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
.leaflet-control-attribution{display:none!important}
.leaflet-tile-pane{filter:saturate(0.2) brightness(0.4) contrast(1.2)}
</style>
</head>
<body>
<div class="header">
<div style="display:flex;align-items:center">
<h1>World Map Live Email Flux</h1>
<span class="sub">Données temps réel depuis la base — zéro simulation</span>
</div>
<div class="clock" id="clock"></div>
<div style="display:flex;align-items:center;gap:8px">
<span class="dot dot-live"></span>
<span style="font-size:12px;color:#66bb6a">LIVE DB</span>
</div>
</div>
<div class="main">
<div class="panel panel-left">
<h3>Contacts par pays</h3>
<div id="countries">Chargement...</div>
</div>
<div id="map"></div>
<div class="panel panel-right">
<h3>Live Feed — Tracking Events</h3>
<div id="feed">Chargement...</div>
</div>
</div>
<div class="bottom-bar" id="kpis">
<div class="kpi"><div class="kpi-val" id="k-sends"></div><div class="kpi-label">Envois Total</div></div>
<div class="kpi"><div class="kpi-val green" id="k-inbox"></div><div class="kpi-label">Inbox Rate</div></div>
<div class="kpi"><div class="kpi-val blue" id="k-isps"></div><div class="kpi-label">ISPs Actifs</div></div>
<div class="kpi"><div class="kpi-val" id="k-methods"></div><div class="kpi-label">Méthodes</div></div>
<div class="kpi"><div class="kpi-val blue" id="k-contacts"></div><div class="kpi-label">Contacts</div></div>
<div class="kpi"><div class="kpi-val" id="k-senders"></div><div class="kpi-label">Senders</div></div>
<div class="kpi"><div class="kpi-val red" id="k-bounce"></div><div class="kpi-label">Bounce Rate</div></div>
<div class="kpi"><div class="kpi-val blue" id="k-countries"></div><div class="kpi-label">Pays</div></div>
</div>
<script>
const API = '/api/realtime-stats.php';
const FLAGS = {US:'🇺🇸','United States':'🇺🇸',Germany:'🇩🇪',France:'🇫🇷',UK:'🇬🇧','United Kingdom':'🇬🇧',Sweden:'🇸🇪',Italy:'🇮🇹',Spain:'🇪🇸',Netherlands:'🇳🇱',Belgium:'🇧🇪',Switzerland:'🇨🇭',Russia:'🇷🇺',Canada:'🇨🇦',Brazil:'🇧🇷',Morocco:'🇲🇦',Tunisia:'🇹🇳',Algeria:'🇩🇿',Portugal:'🇵🇹',Poland:'🇵🇱',Austria:'🇦🇹',Norway:'🇳🇴',Denmark:'🇩🇰',Finland:'🇫🇮',Ireland:'🇮🇪',Japan:'🇯🇵',China:'🇨🇳',India:'🇮🇳',Australia:'🇦🇺',Mexico:'🇲🇽',SWEDEN:'🇸🇪',FRANCE:'🇫🇷',GERMANY:'🇩🇪'};
const COORDS = {US:[39,-98],'United States':[39,-98],Germany:[51,10],France:[46,2],UK:[54,-2],'United Kingdom':[54,-2],Sweden:[62,15],Italy:[42,12],Spain:[40,-4],Netherlands:[52,5],Belgium:[50,4],Switzerland:[47,8],Russia:[55,37],Canada:[56,-106],Brazil:[-14,-51],Morocco:[32,-5],Tunisia:[34,9],Algeria:[28,2],Portugal:[39,-8],Poland:[52,20],Austria:[47,14],Norway:[62,10],Denmark:[56,10],Finland:[64,26],Ireland:[53,-8],Japan:[36,138],China:[35,105],India:[20,77],Australia:[-25,134],Mexico:[23,-102],SWEDEN:[62,15],FRANCE:[46,2],GERMANY:[51,10]};
function N(n){if(!n||n==0)return'0';if(n>=1e6)return(n/1e6).toFixed(1)+'M';if(n>=1e3)return(n/1e3).toFixed(0)+'K';return n.toString()}
// Clock
setInterval(()=>{const d=new Date();document.getElementById('clock').textContent=d.toLocaleTimeString('fr-FR')},1000);
document.getElementById('clock').textContent=new Date().toLocaleTimeString('fr-FR');
// Map
const map = L.map('map',{zoomControl:false,attributionControl:false}).setView([48,10],4);
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png',{maxZoom:18}).addTo(map);
L.control.zoom({position:'bottomright'}).addTo(map);
const markers = L.layerGroup().addTo(map);
async function fetchJSON(q){try{const r=await fetch(API+'?q='+q);return await r.json()}catch(e){return null}}
async function loadOverview(){
const d = await fetchJSON('overview');
if(!d) return;
document.getElementById('k-sends').textContent = N(d.sends_total);
document.getElementById('k-inbox').textContent = d.inbox_rate+'%';
document.getElementById('k-isps').textContent = d.isps_active;
document.getElementById('k-methods').textContent = d.methods||'5';
document.getElementById('k-contacts').textContent = N(d.contacts_total);
document.getElementById('k-senders').textContent = N(d.senders_total);
document.getElementById('k-bounce').textContent = d.bounce_rate+'%';
document.getElementById('k-countries').textContent = d.countries;
}
async function loadCountries(){
const rows = await fetchJSON('by_country');
if(!rows||!rows.length){document.getElementById('countries').textContent='Pas de données';return}
const max = Math.max(...rows.map(r=>parseInt(r.cnt)));
let html = '';
rows.forEach(r=>{
const name = r.country;
const cnt = parseInt(r.cnt);
const pct = Math.round(cnt/max*100);
const flag = FLAGS[name]||'🌍';
html += `<div class="country-row">
<span class="country-flag">${flag}</span>
<span class="country-name">${name}</span>
<span class="country-cnt">${N(cnt)}</span>
<div class="country-bar"><div class="country-bar-fill" style="width:${pct}%"></div></div>
</div>`;
});
document.getElementById('countries').innerHTML = html;
// Add map markers
markers.clearLayers();
rows.forEach(r=>{
const coord = COORDS[r.country];
if(!coord) return;
const cnt = parseInt(r.cnt);
const radius = Math.max(8, Math.min(40, Math.sqrt(cnt/max)*40));
L.circleMarker(coord,{
radius: radius,
fillColor:'#42a5f5',
fillOpacity:0.25,
color:'#42a5f5',
weight:1,
opacity:0.5
}).bindPopup(`<b>${r.country}</b><br>${N(cnt)} contacts`).addTo(markers);
});
}
async function loadFeed(){
const rows = await fetchJSON('recent_events');
if(!rows||!rows.length){document.getElementById('feed').textContent='Aucun événement';return}
let html = '';
rows.forEach(r=>{
const time = new Date(r.created_at).toLocaleTimeString('fr-FR',{hour:'2-digit',minute:'2-digit',second:'2-digit'});
const type = r.event_type||'unknown';
const cls = type==='open'?'feed-open':type==='click'?'feed-click':type==='send'?'feed-send':'feed-bounce';
const label = type.toUpperCase();
html += `<div class="feed-item">
<span class="feed-time">${time}</span>
<span class="feed-type ${cls}">${label}</span>
<span class="feed-tid">${r.tracking_id||'—'}</span>
</div>`;
});
document.getElementById('feed').innerHTML = html;
}
// Initial load
loadOverview();
loadCountries();
loadFeed();
// Refresh every 30s
setInterval(loadOverview, 30000);
setInterval(loadFeed, 15000);
setInterval(loadCountries, 60000);
</script>
<!-- === 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 === -->
</body>
</html>