378 lines
20 KiB
HTML
378 lines
20 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="fr"><head>
|
||
<meta charset="UTF-8">
|
||
<title>Visual Management · WEVAL Consulting</title>
|
||
<style>
|
||
:root {
|
||
--bg:#0a0e27; --panel:#141933; --border:#263161; --text:#e4e8f7; --muted:#9ca8d3;
|
||
--green:#10b981; --amber:#f59e0b; --red:#ef4444; --blue:#6ba3ff; --purple:#c084fc;
|
||
}
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); padding: 20px; min-height: 100vh; line-height: 1.4; }
|
||
.header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 16px; border-bottom: 2px solid #1e3a8a; margin-bottom: 20px; }
|
||
.header h1 { color: var(--blue); font-size: 26px; }
|
||
.header .sub { color: var(--muted); font-size: 12px; margin-top: 4px; }
|
||
.health-badge { padding: 14px 28px; border-radius: 12px; font-size: 28px; font-weight: bold; text-align: center; min-width: 180px; }
|
||
.health-badge .label { font-size: 11px; display: block; text-transform: uppercase; opacity: 0.8; margin-bottom: 4px; }
|
||
.health-green { background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: #fff; }
|
||
.health-amber { background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); color: #fff; }
|
||
.health-red { background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); color: #fff; }
|
||
.section { margin: 24px 0; }
|
||
.section h2 { color: var(--purple); font-size: 18px; margin-bottom: 12px; padding: 8px 0; border-bottom: 1px dashed var(--border); display: flex; align-items: center; gap: 8px; }
|
||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 14px; }
|
||
.kpi { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 16px; position: relative; overflow: hidden; }
|
||
.kpi::before { content: ''; position: absolute; top: 0; left: 0; width: 4px; height: 100%; background: var(--blue); }
|
||
.kpi.green::before { background: var(--green); }
|
||
.kpi.amber::before { background: var(--amber); }
|
||
.kpi.red::before { background: var(--red); }
|
||
.kpi .label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; }
|
||
.kpi .value { font-size: 28px; font-weight: bold; color: var(--text); font-family: 'SF Mono', Monaco, monospace; }
|
||
.kpi .unit { font-size: 13px; color: var(--muted); margin-left: 4px; font-weight: normal; }
|
||
.kpi .sub { font-size: 11px; color: var(--muted); margin-top: 4px; }
|
||
.andon { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; background: var(--panel); border: 1px solid var(--border); border-left-width: 5px; border-radius: 6px; margin-bottom: 8px; }
|
||
.andon.RED { border-left-color: var(--red); background: linear-gradient(90deg, rgba(239,68,68,0.1) 0%, var(--panel) 100%); }
|
||
.andon.ORANGE { border-left-color: var(--amber); background: linear-gradient(90deg, rgba(245,158,11,0.1) 0%, var(--panel) 100%); }
|
||
.andon .sev { font-weight: bold; padding: 3px 10px; border-radius: 4px; font-size: 11px; }
|
||
.andon.RED .sev { background: var(--red); color: #fff; }
|
||
.andon.ORANGE .sev { background: var(--amber); color: #000; }
|
||
.andon .msg { flex: 1; margin-left: 16px; font-size: 13px; }
|
||
.andon .kpi-name { color: var(--muted); font-size: 11px; font-family: 'SF Mono', monospace; }
|
||
.footer { color: var(--muted); font-size: 11px; margin-top: 28px; text-align: right; padding-top: 16px; border-top: 1px dashed var(--border); }
|
||
.links { display: flex; gap: 12px; flex-wrap: wrap; margin: 10px 0; }
|
||
.links a { color: var(--blue); text-decoration: none; font-size: 12px; padding: 4px 10px; background: var(--panel); border-radius: 4px; border: 1px solid var(--border); }
|
||
.links a:hover { background: var(--border); }
|
||
.skeleton { display: inline-block; width: 60px; height: 24px; background: linear-gradient(90deg, var(--border), #1e2549, var(--border)); background-size: 200% 100%; animation: skel 1.5s infinite; border-radius: 4px; }
|
||
@keyframes skel { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
|
||
#toast { position: fixed; bottom: 20px; right: 20px; padding: 12px 20px; background: var(--red); color: #fff; border-radius: 6px; display: none; z-index: 1000; font-size: 13px; }
|
||
.no-andon { color: var(--green); padding: 20px; text-align: center; font-size: 14px; background: var(--panel); border-radius: 6px; border: 1px dashed var(--green); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="header">
|
||
<div>
|
||
<h1>📊 Visual Management · WEVAL</h1>
|
||
<div class="sub">Doctrine 65 — Lean Six Sigma · Andon alerts · Auto-refresh 30s · 0 hardcode</div>
|
||
</div>
|
||
<div class="health-badge health-amber" id="health-badge">
|
||
<span class="label">Health Score</span>
|
||
<span id="health-value">—</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="links">
|
||
<a href="/dashboards-hub.html">🏠 Hub</a>
|
||
<a href="/crm-dashboard-live.html">💼 CRM</a>
|
||
<a href="/contacts-segmentation-dashboard.html">🎯 B2B/B2C</a>
|
||
<a href="/ethica-dashboard-live.html">🏥 Ethica</a>
|
||
<a href="/office-365-dashboard-live.html">📮 O365</a>
|
||
<a href="/infra-dashboard-live.html">⚙️ Infra</a>
|
||
<a href="/database-dashboard-live.html">🗄️ DB</a>
|
||
<a href="/wevia-master.html">🤖 WEVIA Chat</a>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h2>🚨 Andon Alerts</h2>
|
||
<div id="andon-list"><div class="skeleton" style="width:100%;height:60px"></div></div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h2>💼 Business KPIs — CRM Pipeline</h2>
|
||
<div class="grid" id="kpi-business"></div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h2>🌊 Flux KPIs — Flow Detection (doctrine 55 anti-staleness)</h2>
|
||
<div class="grid" id="kpi-flux"></div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h2>🏥 Ethica HCPs</h2>
|
||
<div class="grid" id="kpi-ethica"></div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h2>📮 Office 365 Email Infrastructure</h2>
|
||
<div class="grid" id="kpi-office"></div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h2>✅ Quality — Lean Six Sigma</h2>
|
||
<div class="grid" id="kpi-quality"></div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h2>🎯 B2B/B2C Classification (doctrine 63)</h2>
|
||
<div class="grid" id="kpi-classif"></div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h2>⚙️ Infra Live</h2>
|
||
<div class="grid" id="kpi-infra"></div>
|
||
</div>
|
||
|
||
<div class="footer" id="footer">Connecting...</div>
|
||
<div id="toast"></div>
|
||
|
||
<script>
|
||
const fmt = n => (n == null) ? '—' : Number(n).toLocaleString('fr-FR');
|
||
const fmtPct = n => n == null ? '—' : Number(n).toFixed(1) + '%';
|
||
const fmtMoney = n => n == null ? '—' : (Math.round(n/1000) + 'k€');
|
||
|
||
function showToast(msg) {
|
||
const t = document.getElementById('toast');
|
||
t.textContent = msg;
|
||
t.style.display = 'block';
|
||
setTimeout(() => t.style.display = 'none', 5000);
|
||
}
|
||
|
||
function kpiCard(label, value, unit='', sub='', status='') {
|
||
return `<div class="kpi ${status}">
|
||
<div class="label">${label}</div>
|
||
<div class="value">${value}<span class="unit">${unit}</span></div>
|
||
${sub ? `<div class="sub">${sub}</div>` : ''}
|
||
</div>`;
|
||
}
|
||
|
||
async function loadData() {
|
||
try {
|
||
const r = await fetch('/api/visual-management-live.php');
|
||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||
const d = await r.json();
|
||
|
||
// Health badge
|
||
const badge = document.getElementById('health-badge');
|
||
badge.className = 'health-badge health-' + (d.health_status || 'amber').toLowerCase();
|
||
document.getElementById('health-value').textContent = (d.health_score || 0) + '/100';
|
||
|
||
// Andons
|
||
const alist = document.getElementById('andon-list');
|
||
alist.innerHTML = '';
|
||
if (!d.andons || d.andons.length === 0) {
|
||
alist.innerHTML = '<div class="no-andon">✅ Aucune alerte Andon — système healthy</div>';
|
||
} else {
|
||
d.andons.forEach(a => {
|
||
alist.innerHTML += `<div class="andon ${a.severity}">
|
||
<span class="sev">${a.severity}</span>
|
||
<span class="msg">${a.message}</span>
|
||
<span class="kpi-name">${a.kpi}</span>
|
||
</div>`;
|
||
});
|
||
}
|
||
|
||
// Business
|
||
const b = d.business || {};
|
||
const statDeals = b.crm_deals < 5 ? 'red' : 'green';
|
||
document.getElementById('kpi-business').innerHTML =
|
||
kpiCard('Deals', fmt(b.crm_deals), '', `Valeur: ${fmtMoney(b.crm_deals_amount_eur)}`, statDeals) +
|
||
kpiCard('Companies', fmt(b.crm_companies), '', 'Pipeline B2B', 'green') +
|
||
kpiCard('Contacts B2B', fmt(b.crm_contacts_b2b), '', `${fmt(b.crm_contacts_linked)} linked`, 'green') +
|
||
kpiCard('Activities', fmt(b.crm_activities), '', `${fmt(b.crm_activities_last_7d)} (7j)`, b.crm_activities < 100 ? 'amber' : 'green') +
|
||
kpiCard('Leads (7.3M)', fmt(b.crm_contacts + 7354710), '', 'leads + contacts pool');
|
||
|
||
// Flux
|
||
const f = d.flux || {};
|
||
const dateLastSend = f.send_contacts_last_created ? f.send_contacts_last_created.split(' ')[0] : '—';
|
||
const dateLastGraph = f.graph_send_last_created ? f.graph_send_last_created.split(' ')[0] : '—';
|
||
document.getElementById('kpi-flux').innerHTML =
|
||
kpiCard('send_contacts 7j', fmt(f.send_contacts_last_7d), '', `Dernière: ${dateLastSend}`, f.send_contacts_last_7d === 0 ? 'red' : 'green') +
|
||
kpiCard('send_contacts 30j', fmt(f.send_contacts_last_30d), '', 'Flux legacy CRM', 'amber') +
|
||
kpiCard('graph_send 7j', fmt(f.graph_send_last_7d), '', `Dernière: ${dateLastGraph}`, f.graph_send_last_7d < 100 ? 'amber' : 'green') +
|
||
kpiCard('weval_leads 7j', fmt(f.weval_leads_last_7d), '', 'B2B scraper flux', f.weval_leads_last_7d > 100 ? 'green' : 'amber') +
|
||
kpiCard('pipeline_deals 30j', fmt(f.pipeline_deals_last_30d), '', 'Nouveaux deals', f.pipeline_deals_last_30d === 0 ? 'red' : 'green') +
|
||
kpiCard('pipeline_contacts 24h', fmt(f.pipeline_contacts_last_24h), '', 'Import récent', 'green');
|
||
|
||
// Ethica
|
||
document.getElementById('kpi-ethica').innerHTML =
|
||
kpiCard('HCPs Total', fmt(b.ethica_hcps), '', '3 pays Maghreb+INT') +
|
||
kpiCard('DZ Algérie', fmt(b.ethica_hcps_dz), '', 'Campaign ready', 'green') +
|
||
kpiCard('MA Maroc', fmt(b.ethica_hcps_ma), '', 'Active market', 'green') +
|
||
kpiCard('TN Tunisie', fmt(b.ethica_hcps_tn), '', 'Secondary market', 'green');
|
||
|
||
// Office
|
||
document.getElementById('kpi-office').innerHTML =
|
||
kpiCard('Accounts', fmt(b.office_accounts), '', '9 tenants') +
|
||
kpiCard('Actifs', fmt(b.office_active), '', 'Ready to send', 'green') +
|
||
kpiCard('Warming', fmt(b.office_warming), '', 'En préchauffe', 'amber');
|
||
|
||
// Quality
|
||
const qu = d.quality || {};
|
||
document.getElementById('kpi-quality').innerHTML =
|
||
kpiCard('NonReg', fmt(qu.nonreg_pass) + '/' + fmt(qu.nonreg_total), '', fmtPct(qu.nonreg_score) + ' score', qu.nonreg_score === 100 ? 'green' : 'red') +
|
||
kpiCard('L99', fmt(qu.l99_pass) + '/' + fmt(qu.l99_total), '', fmtPct(qu.l99_score) + ' score', qu.l99_score === 100 ? 'green' : 'red');
|
||
|
||
// Classification
|
||
const c = d.classification || {};
|
||
document.getElementById('kpi-classif').innerHTML =
|
||
kpiCard('Leads classified', fmtPct(c.leads_classified_pct), '', `B2B: ${fmt(c.leads_b2b)}`, c.leads_classified_pct === 100 ? 'green' : 'amber') +
|
||
kpiCard('Send contacts classified', fmtPct(c.send_contacts_classified_pct), '', `B2B: ${fmt(c.send_contacts_b2b)}`, c.send_contacts_classified_pct === 100 ? 'green' : 'amber');
|
||
|
||
// Infra
|
||
const i = d.infra || {};
|
||
document.getElementById('kpi-infra').innerHTML =
|
||
kpiCard('Load 1m', i.load_1m, '', `5m: ${i.load_5m}`, i.load_1m < 4 ? 'green' : 'amber') +
|
||
kpiCard('Memory', fmtPct(i.mem_pct), '', 'S204', i.mem_pct < 70 ? 'green' : 'amber') +
|
||
kpiCard('Disk', fmtPct(i.disk_pct), '', '150G total', i.disk_pct < 80 ? 'green' : 'amber') +
|
||
kpiCard('Docker', fmt(i.docker_containers), '', 'Containers UP', 'green') +
|
||
kpiCard('Uptime', fmt(i.uptime_hours), 'h', 'Since last reboot');
|
||
|
||
// Footer
|
||
document.getElementById('footer').textContent =
|
||
`Last refresh: ${new Date(d.ts).toLocaleString('fr-FR')} · Andons: ${d.andons_count || 0} · API: /api/visual-management-live.php · Auto-refresh 30s`;
|
||
} catch (e) {
|
||
showToast('Erreur: ' + e.message);
|
||
document.getElementById('footer').textContent = 'Error: ' + e.message;
|
||
}
|
||
}
|
||
loadData();
|
||
setInterval(loadData, 30000);
|
||
</script>
|
||
|
||
<!-- KPI HISTORY CHART 30J (wired 17avr by Opus doctrine 60 UX premium) -->
|
||
<div id="kpi-history-chart-30d" style="margin:2rem 1rem;padding:1.5rem;background:linear-gradient(135deg,rgba(20,25,40,0.9),rgba(30,40,60,0.85));border-radius:16px;border:1px solid rgba(100,140,200,0.2);backdrop-filter:blur(12px);">
|
||
<h3 style="color:#e0e8ff;font:600 1.1rem system-ui;margin:0 0 1rem;">Historique KPI 30 jours</h3>
|
||
<canvas id="kpiHistCanvas" style="max-height:320px;"></canvas>
|
||
<div id="kpiHistStatus" style="color:#8899bb;font:400 0.85rem system-ui;margin-top:0.5rem;"></div>
|
||
</div>
|
||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
|
||
<script>
|
||
(async function(){
|
||
try {
|
||
const r = await fetch('/api/kpi-history-30d.php');
|
||
const j = await r.json();
|
||
if (!j.data || !j.data.length) { document.getElementById('kpiHistStatus').textContent = 'Snapshot quotidien actif (0 2 * * *). Données à partir de demain.'; return; }
|
||
const labels = j.data.map(d => d.snap_date);
|
||
const ds = [
|
||
{label:'CRM contacts', data: j.data.map(d=>+d.crm_contacts), borderColor:'#4a9eff', tension:0.3},
|
||
{label:'CRM companies', data: j.data.map(d=>+d.crm_companies), borderColor:'#50e090', tension:0.3},
|
||
{label:'CRM activities', data: j.data.map(d=>+d.crm_activities), borderColor:'#f0a050', tension:0.3},
|
||
{label:'Ethica HCPs', data: j.data.map(d=>+d.ethica_hcps), borderColor:'#b070ff', tension:0.3},
|
||
{label:'Office active', data: j.data.map(d=>+d.office_active), borderColor:'#ff6090', tension:0.3},
|
||
{label:'Health score', data: j.data.map(d=>+d.health_score), borderColor:'#ffc040', tension:0.3, yAxisID:'y2'}
|
||
];
|
||
new Chart(document.getElementById('kpiHistCanvas').getContext('2d'), {
|
||
type:'line',
|
||
data:{labels, datasets:ds},
|
||
options:{
|
||
responsive:true, maintainAspectRatio:false,
|
||
plugins:{legend:{labels:{color:'#c0c8d8',font:{size:11}}}},
|
||
scales:{
|
||
x:{ticks:{color:'#8899bb'},grid:{color:'rgba(100,120,160,0.1)'}},
|
||
y:{type:'logarithmic',ticks:{color:'#8899bb'},grid:{color:'rgba(100,120,160,0.1)'}},
|
||
y2:{position:'right',min:0,max:100,ticks:{color:'#ffc040'},grid:{display:false}}
|
||
}
|
||
}
|
||
});
|
||
document.getElementById('kpiHistStatus').textContent = `${j.count} points, généré ${j.generated_at}`;
|
||
} catch(e){ document.getElementById('kpiHistStatus').textContent = 'Erreur chargement: '+e.message; }
|
||
})();
|
||
</script>
|
||
<!-- END KPI HISTORY CHART 30J -->
|
||
|
||
<!-- WTP-GAP-FILL-V1 (doctrine 90-v2 gap-fill showcase, 18avr 2026) -->
|
||
<style>
|
||
.wtp-gapfill-banner{position:fixed;bottom:0;left:0;right:0;z-index:99999;background:linear-gradient(90deg,#05060a,#0b0d15 20%,#181d2e 50%,#0b0d15 80%,#05060a);border-top:2px solid #14b8a6;color:#e2e8f0;padding:10px 16px;font-family:Inter,system-ui,-apple-system,sans-serif;font-size:11.5px;display:flex;align-items:center;gap:12px;flex-wrap:wrap;box-shadow:0 -10px 30px rgba(20,184,166,.28)}
|
||
.wtp-gapfill-banner a{color:#5eead4;text-decoration:none;font-weight:600;transition:color .15s}
|
||
.wtp-gapfill-banner a:hover{color:#22d3ee}
|
||
.wtp-gapfill-banner .pill{padding:2px 9px;background:rgba(99,102,241,.14);color:#a5b4fc;border-radius:10px;font-size:10.5px;font-family:JetBrains Mono,monospace;font-weight:600}
|
||
.wtp-gapfill-banner .pill.new{background:rgba(20,184,166,.22);color:#5eead4}
|
||
.wtp-gapfill-banner .pill.hot{background:rgba(236,72,153,.22);color:#f472b6}
|
||
.wtp-gapfill-banner .close{margin-left:auto;cursor:pointer;color:#64748b;padding:0 8px;font-size:16px;line-height:1;border:1px solid #334155;border-radius:4px}
|
||
.wtp-gapfill-banner .close:hover{color:#e2e8f0;border-color:#64748b}
|
||
.wtp-gapfill-banner.hidden{display:none}
|
||
@media(max-width:768px){.wtp-gapfill-banner{font-size:10px;padding:7px 10px;gap:8px}}
|
||
</style>
|
||
<div class="wtp-gapfill-banner" id="wtpGapFillBanner">
|
||
<span>🎯 <strong>WEVAL Agents Gap-Fill ERP</strong></span>
|
||
<span class="pill hot">45 gaps</span>
|
||
<span class="pill">SAP · Oracle · NetSuite · Dynamics</span>
|
||
<span class="pill new">🆕 Meeting Rooms</span>
|
||
<span class="pill new">🆕 Lean 6 Sigma</span>
|
||
<span id="wtp-gfb-metrics" class="pill">— chargement —</span>
|
||
<a href="/weval-technology-platform.html">→ WTP Portal (16 mod)</a>
|
||
<a href="/enterprise-model.html">Enterprise Model</a>
|
||
<a href="/api/weval-agents-gap-fill-manifest.json" target="_blank">📋 Manifest</a>
|
||
<span class="close" onclick="document.getElementById("wtpGapFillBanner").classList.add("hidden");localStorage.setItem("wtpGapFillHidden","1")">×</span>
|
||
</div>
|
||
<script>
|
||
(async()=>{
|
||
if(localStorage.getItem("wtpGapFillHidden")==="1"){document.getElementById("wtpGapFillBanner").classList.add("hidden");return;}
|
||
try{
|
||
const r=await fetch("/api/source-of-truth.json?t="+Date.now());
|
||
const d=await r.json();
|
||
const el=document.getElementById("wtp-gfb-metrics");
|
||
if(el)el.textContent=(d.ethica_total||"?")+" HCPs · "+(d.nonreg||"?")+" · "+(d.providers_count||"?")+" IA · "+(d.agents_count||"?")+" agents · "+(d.docker_running||"?")+" 🐳";
|
||
}catch(e){}
|
||
})();
|
||
</script>
|
||
<!-- VM Alerts DG Widget 18avr Opus Yacine · enriches with 7 alerts + 6 risks + 8 stats from /api/wevia-v69-dg-command-center.php -->
|
||
<div id="dsh-vm-alerts-mount"></div>
|
||
<script src="/dsh-vm-alerts-widget.js" defer></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>
|