185 lines
9.4 KiB
HTML
185 lines
9.4 KiB
HTML
<!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>
|
||
</body>
|
||
</html>
|