394 lines
20 KiB
HTML
394 lines
20 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="fr">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>WEVAL · Visual Mgmt Drillable · 9 modules graphique</title>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.umd.min.js"></script>
|
||
<style>
|
||
*{margin:0;padding:0;box-sizing:border-box}
|
||
:root{
|
||
--bg:#09090b; --surface:#13131a; --surface2:#1a1a24; --border:#2a2a35;
|
||
--text:#fafafa; --muted:#94a3b8; --accent:#6366f1; --accent2:#a855f7;
|
||
--green:#22c55e; --amber:#f59e0b; --red:#ef4444; --cyan:#22d3ee;
|
||
--grad:linear-gradient(135deg,#6366f1 0%,#a855f7 50%,#ec4899 100%);
|
||
}
|
||
body{font-family:-apple-system,Inter,Segoe UI,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;line-height:1.5}
|
||
.topbar{background:var(--surface);border-bottom:1px solid var(--border);padding:14px 28px;display:flex;justify-content:space-between;align-items:center;position:sticky;top:0;z-index:100;backdrop-filter:blur(20px)}
|
||
.topbar h1{font-size:1.1rem;font-weight:700;background:var(--grad);-webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:-0.02em}
|
||
.topbar .meta{display:flex;gap:14px;align-items:center;font-size:.78rem;color:var(--muted)}
|
||
.topbar .live{display:inline-flex;align-items:center;gap:6px;color:var(--green);font-weight:600}
|
||
.topbar .live::before{content:'';width:8px;height:8px;background:var(--green);border-radius:99px;box-shadow:0 0 12px var(--green);animation:pulse 1.5s infinite}
|
||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
|
||
.bread{padding:14px 28px;background:var(--surface);font-size:.78rem;color:var(--muted);border-bottom:1px solid var(--border)}
|
||
.bread a{color:var(--accent);text-decoration:none;margin-right:6px}
|
||
main{padding:28px;max-width:1700px;margin:0 auto}
|
||
.hero{background:linear-gradient(135deg,rgba(99,102,241,.1),rgba(168,85,247,.05));border:1px solid rgba(99,102,241,.2);border-radius:16px;padding:24px 28px;margin-bottom:24px;position:relative;overflow:hidden}
|
||
.hero::before{content:'';position:absolute;top:-50%;right:-10%;width:300px;height:300px;background:radial-gradient(circle,rgba(99,102,241,.15),transparent 70%);pointer-events:none}
|
||
.hero h2{font-size:1.4rem;margin-bottom:6px;background:var(--grad);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
||
.hero p{color:var(--muted);font-size:.85rem;line-height:1.6}
|
||
.modules-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(330px,1fr));gap:18px;margin-bottom:24px}
|
||
.module-card{background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:0;position:relative;overflow:hidden;cursor:pointer;transition:.25s;display:flex;flex-direction:column}
|
||
.module-card::before{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:var(--grad);opacity:.6}
|
||
.module-card:hover{border-color:var(--accent);transform:translateY(-2px);box-shadow:0 14px 35px rgba(99,102,241,.18)}
|
||
.module-card.expanded{grid-column:span 2;cursor:default;border-color:var(--accent);box-shadow:0 14px 40px rgba(99,102,241,.25)}
|
||
@media(max-width:900px){.module-card.expanded{grid-column:span 1}}
|
||
.module-header{padding:18px 20px;display:flex;justify-content:space-between;align-items:flex-start;gap:10px}
|
||
.module-title{font-size:1rem;font-weight:700;color:var(--text);display:flex;align-items:center;gap:8px}
|
||
.module-title .icon{font-size:1.4rem;line-height:1}
|
||
.module-status{font-size:.65rem;font-weight:700;padding:3px 8px;border-radius:99px;text-transform:uppercase;letter-spacing:.5px}
|
||
.status-green{background:rgba(34,197,94,.15);color:var(--green)}
|
||
.status-amber{background:rgba(245,158,11,.15);color:var(--amber)}
|
||
.status-red{background:rgba(239,68,68,.15);color:var(--red)}
|
||
.status-grey{background:rgba(148,163,184,.15);color:var(--muted)}
|
||
.module-body{padding:0 20px 16px}
|
||
.kpi-row{display:flex;gap:14px;margin-bottom:10px;flex-wrap:wrap}
|
||
.kpi-mini{flex:1;min-width:90px;background:var(--surface2);border-radius:8px;padding:8px 10px}
|
||
.kpi-mini .lbl{font-size:.65rem;color:var(--muted);text-transform:uppercase;letter-spacing:.8px;font-weight:600}
|
||
.kpi-mini .val{font-size:1.2rem;font-weight:700;color:var(--text);line-height:1.2;font-variant-numeric:tabular-nums}
|
||
.expand-hint{font-size:.7rem;color:var(--muted);text-align:right;padding:8px 16px;border-top:1px solid var(--border);background:var(--surface2);font-weight:500}
|
||
.expand-hint::before{content:'▾ ';color:var(--accent)}
|
||
.expanded .expand-hint::before{content:'▴ '}
|
||
.drill-content{display:none;padding:16px 20px 20px;border-top:1px solid var(--border);background:#0c0c12;border-radius:0 0 14px 14px}
|
||
.expanded .drill-content{display:block;animation:slideIn .3s ease}
|
||
@keyframes slideIn{from{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}
|
||
.drill-section-title{font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:1.2px;color:var(--muted);margin:14px 0 8px;display:flex;align-items:center;gap:6px}
|
||
.drill-section-title::after{content:'';flex:1;height:1px;background:var(--border)}
|
||
.chart-wrap{position:relative;height:180px;margin:8px 0}
|
||
.chart-wrap-sm{position:relative;height:130px;margin:8px 0}
|
||
.dual-charts{display:grid;grid-template-columns:1fr 1fr;gap:14px}
|
||
@media(max-width:700px){.dual-charts{grid-template-columns:1fr}}
|
||
.row{display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px dashed var(--border);font-size:.78rem}
|
||
.row:last-child{border-bottom:none}
|
||
.row .lbl{color:var(--muted)}
|
||
.row .v{color:var(--text);font-weight:600;font-variant-numeric:tabular-nums}
|
||
.module-actions{display:flex;gap:6px;margin-top:10px;flex-wrap:wrap}
|
||
.btn-mini{padding:5px 10px;background:var(--surface);border:1px solid var(--border);border-radius:6px;color:var(--muted);font-size:.7rem;cursor:pointer;font-weight:600;transition:.2s;text-decoration:none}
|
||
.btn-mini:hover{background:var(--accent);color:#fff;border-color:var(--accent)}
|
||
footer{padding:32px 28px;text-align:center;color:var(--muted);font-size:.75rem;border-top:1px solid var(--border);margin-top:32px}
|
||
footer a{color:var(--accent);text-decoration:none;margin:0 8px}
|
||
.refresh{padding:5px 12px;background:var(--surface2);border:1px solid var(--border);border-radius:99px;color:var(--muted);font-size:.7rem;cursor:pointer}
|
||
.refresh:hover{border-color:var(--accent);color:var(--text)}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="topbar">
|
||
<h1>📊 WEVAL · Visual Mgmt Drillable · 9 modules</h1>
|
||
<div class="meta">
|
||
<span>Source: <a href="/api/wevia-kpi-feeders.php" style="color:var(--cyan)">kpi-feeders</a> + <a href="/api/wevia-truth-registry.json" style="color:var(--cyan)">truth-registry</a></span>
|
||
<span>Refresh: <span id="ts">—</span></span>
|
||
<button class="refresh" onclick="loadAll()">↻</button>
|
||
<span class="live">LIVE</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="bread">
|
||
<a href="/">Home</a> /
|
||
<a href="/weval-technology-platform.html">WTP (point d'entrée)</a> /
|
||
<a href="/wevia-erp-v2.html">ERP V2</a> /
|
||
<span style="color:var(--text)">Visual Mgmt Drillable</span>
|
||
</div>
|
||
|
||
<main>
|
||
|
||
<div class="hero">
|
||
<h2>📊 Management Visuel Drillable · 9 Modules ERP</h2>
|
||
<p>Click sur module pour drill-down · graphiques Chart.js premium · IDs uniques par module · auto-refresh 60s · zero hardcode (live data) · zero écrasement (nouvelle page complémentaire) · doctrine 60 UX premium · TOUT graphique pas que tableaux</p>
|
||
</div>
|
||
|
||
<div class="modules-grid" id="modules-grid">
|
||
<!-- 9 cards rendered by JS, each with id="dd-<module>" -->
|
||
</div>
|
||
|
||
</main>
|
||
|
||
<footer>
|
||
WEVAL Visual Mgmt Drillable · v1.0 · Chart.js 4.4.0 · 9 modules · IDs uniques dd-* ·
|
||
<a href="/weval-technology-platform.html">WTP</a> ·
|
||
<a href="/wevia-erp-v2.html">ERP V2</a> ·
|
||
<a href="/wevia-erp-unified.html">ERP V1</a> ·
|
||
<a href="/visual-management.html">Visual Mgmt v1</a> ·
|
||
<a href="/api/wevia-kpi-feeders.php">API KPI Feeders</a>
|
||
</footer>
|
||
|
||
<script>
|
||
Chart.defaults.color = '#94a3b8';
|
||
Chart.defaults.borderColor = '#2a2a35';
|
||
Chart.defaults.font.family = '-apple-system,Inter,sans-serif';
|
||
Chart.defaults.font.size = 10;
|
||
|
||
const COLORS = ['#6366f1','#a855f7','#ec4899','#22d3ee','#22c55e','#f59e0b','#ef4444','#8b5cf6','#06b6d4'];
|
||
|
||
let DATA = null;
|
||
let CHARTS = {};
|
||
|
||
const MODULES = [
|
||
{id: 'finance', icon: '💰', label: 'Finance', kpis: ['revenue_eur','costs_eur','margin_pct','cash_runway_months']},
|
||
{id: 'sales', icon: '📞', label: 'Sales', kpis: ['companies','contacts_b2b','pipeline_deals','pipeline_value']},
|
||
{id: 'supply', icon: '📦', label: 'Supply (Office Acc)', kpis: ['office_accounts_total','office_accounts_active','office_accounts_suspended','health_pct']},
|
||
{id: 'manufacturing', icon: '🏭', label: 'Manufacturing', kpis: []},
|
||
{id: 'rd', icon: '🧠', label: 'R&D', kpis: ['agents','intents','brains','doctrines','autonomy_score']},
|
||
{id: 'hr', icon: '👥', label: 'HR', kpis: ['consultants','candidates']},
|
||
{id: 'marketing', icon: '🏥', label: 'Marketing (Ethica)', kpis: ['ethica_hcps_total','ethica_dz','ethica_ma','ethica_tn']},
|
||
{id: 'it', icon: '🖥', label: 'IT', kpis: ['machines_online','gpu_providers','docker_containers','systemd_services','fpm_workers']},
|
||
{id: 'quality', icon: '🏆', label: 'Quality', kpis: ['nonreg_pass','nonreg_total','l99_pass','l99_total','seven_sigma_pass','seven_sigma_total','dpmo','sigma_level']},
|
||
];
|
||
|
||
function fmt(n) {
|
||
if (n === null || n === undefined) return '—';
|
||
if (typeof n !== 'number') return n;
|
||
if (n >= 1000000) return (n/1000000).toFixed(1)+'M';
|
||
if (n >= 1000) return (n/1000).toFixed(1)+'k';
|
||
return n.toLocaleString('fr-FR');
|
||
}
|
||
|
||
function statusOf(modKey, modData) {
|
||
if (modKey === 'manufacturing') return 'grey';
|
||
if (modKey === 'finance') return (modData.revenue_eur || 0) > 0 ? 'green' : 'amber';
|
||
if (modKey === 'quality') {
|
||
const ok = (modData.nonreg_pass === modData.nonreg_total) && (modData.l99_pass === modData.l99_total);
|
||
return ok ? 'green' : 'red';
|
||
}
|
||
if (modKey === 'rd') return (modData.autonomy_score || 0) >= 80 ? 'green' : 'amber';
|
||
return 'green';
|
||
}
|
||
|
||
async function loadAll() {
|
||
document.getElementById('ts').textContent = new Date().toLocaleTimeString('fr-FR');
|
||
|
||
try {
|
||
const r = await fetch('/api/wevia-kpi-feeders.php');
|
||
DATA = await r.json();
|
||
renderModules();
|
||
} catch (e) {
|
||
console.error(e);
|
||
document.getElementById('modules-grid').innerHTML = '<div style="text-align:center;color:var(--muted);padding:40px">Erreur fetch KPI feeders: ' + e.message + '</div>';
|
||
}
|
||
}
|
||
|
||
function renderModules() {
|
||
if (!DATA) return;
|
||
const modules = DATA.modules || {};
|
||
|
||
// Map module keys (capitalized in API) to lowercase ids
|
||
const modKeyMap = {
|
||
finance: 'Finance', sales: 'Sales', supply: 'Supply', manufacturing: 'Manufacturing',
|
||
rd: 'RD', hr: 'HR', marketing: 'Marketing', it: 'IT', quality: 'Quality'
|
||
};
|
||
|
||
const grid = document.getElementById('modules-grid');
|
||
const wasExpanded = {};
|
||
document.querySelectorAll('.module-card.expanded').forEach(el => wasExpanded[el.id] = true);
|
||
|
||
grid.innerHTML = MODULES.map(m => {
|
||
const apiKey = modKeyMap[m.id] || m.label;
|
||
const modData = modules[apiKey] || {};
|
||
const status = statusOf(m.id, modData);
|
||
|
||
// Top 4 KPIs in summary
|
||
const summaryKpis = m.kpis.slice(0, 4).map(k => {
|
||
const v = modData[k];
|
||
return `<div class="kpi-mini"><div class="lbl">${k.replace(/_/g,' ')}</div><div class="val">${fmt(v)}</div></div>`;
|
||
}).join('');
|
||
|
||
return `
|
||
<div class="module-card" id="dd-${m.id}" onclick="toggleExpand('dd-${m.id}')">
|
||
<div class="module-header">
|
||
<div class="module-title"><span class="icon">${m.icon}</span> ${m.label}</div>
|
||
<span class="module-status status-${status}">${status === 'green' ? '✓ OK' : status === 'amber' ? '⚠ TBD' : status === 'red' ? '✗ ERR' : 'N/A'}</span>
|
||
</div>
|
||
${summaryKpis ? `<div class="module-body"><div class="kpi-row">${summaryKpis}</div></div>` : `<div class="module-body"><div style="color:var(--muted);font-size:.78rem;padding:6px 0">${modData._note || 'Pas de données'}</div></div>`}
|
||
<div class="drill-content" id="dd-${m.id}-drill">
|
||
<div class="drill-section-title">📊 Détails graphiques</div>
|
||
<div class="dual-charts">
|
||
<div><div class="chart-wrap-sm"><canvas id="dd-${m.id}-chart-1"></canvas></div></div>
|
||
<div><div class="chart-wrap-sm"><canvas id="dd-${m.id}-chart-2"></canvas></div></div>
|
||
</div>
|
||
<div class="drill-section-title">🔢 Tous les KPIs</div>
|
||
${m.kpis.map(k => `<div class="row"><span class="lbl">${k.replace(/_/g,' ')}</span><span class="v">${fmt(modData[k])}</span></div>`).join('') || '<div style="color:var(--muted);font-size:.75rem">Aucun KPI</div>'}
|
||
${modData._source ? `<div class="row"><span class="lbl">source</span><span class="v" style="font-size:.7rem">${modData._source}</span></div>` : ''}
|
||
<div class="module-actions">
|
||
<a class="btn-mini" href="/api/wevia-kpi-feeders.php" target="_blank">📡 API JSON</a>
|
||
<a class="btn-mini" href="/wevia-erp-v2.html#${m.id}" target="_blank">📈 ERP V2</a>
|
||
<a class="btn-mini" href="/wevia-master.html?q=${encodeURIComponent(m.label.toLowerCase())}" target="_blank">💬 Ask WEVIA</a>
|
||
</div>
|
||
</div>
|
||
<div class="expand-hint">Click pour drill-down</div>
|
||
</div>`;
|
||
}).join('');
|
||
|
||
// Re-expand previously expanded
|
||
Object.keys(wasExpanded).forEach(id => {
|
||
const el = document.getElementById(id);
|
||
if (el) { el.classList.add('expanded'); renderCharts(id.replace('dd-','')); }
|
||
});
|
||
}
|
||
|
||
function toggleExpand(cardId) {
|
||
const card = document.getElementById(cardId);
|
||
if (!card) return;
|
||
const wasExpanded = card.classList.contains('expanded');
|
||
card.classList.toggle('expanded');
|
||
if (!wasExpanded) {
|
||
renderCharts(cardId.replace('dd-',''));
|
||
} else {
|
||
// Destroy charts to free memory
|
||
['chart-1','chart-2'].forEach(s => {
|
||
const k = `dd-${cardId.replace('dd-','')}-${s}`;
|
||
if (CHARTS[k]) { CHARTS[k].destroy(); delete CHARTS[k]; }
|
||
});
|
||
}
|
||
}
|
||
|
||
function renderCharts(modId) {
|
||
const apiKey = {finance:'Finance',sales:'Sales',supply:'Supply',manufacturing:'Manufacturing',rd:'RD',hr:'HR',marketing:'Marketing',it:'IT',quality:'Quality'}[modId];
|
||
const modData = (DATA?.modules || {})[apiKey] || {};
|
||
const m = MODULES.find(x => x.id === modId);
|
||
if (!m) return;
|
||
|
||
setTimeout(() => { // wait for canvas to mount
|
||
// Chart 1 : bar of all numeric KPIs
|
||
const numericKpis = m.kpis.filter(k => typeof modData[k] === 'number');
|
||
if (numericKpis.length > 0) {
|
||
const c1 = document.getElementById(`dd-${modId}-chart-1`);
|
||
if (c1) {
|
||
const k1 = `dd-${modId}-chart-1`;
|
||
if (CHARTS[k1]) CHARTS[k1].destroy();
|
||
CHARTS[k1] = new Chart(c1, {
|
||
type: 'bar',
|
||
data: {
|
||
labels: numericKpis.map(k => k.replace(/_/g,' ').slice(0,15)),
|
||
datasets: [{
|
||
label: 'Value',
|
||
data: numericKpis.map(k => modData[k]),
|
||
backgroundColor: COLORS.slice(0, numericKpis.length),
|
||
borderRadius: 5,
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true, maintainAspectRatio: false,
|
||
plugins: { legend: {display: false}, title: {display:true, text: 'KPIs Bar', font:{size:10}}},
|
||
scales: {
|
||
y: { beginAtZero: true, type: numericKpis.some(k => modData[k] > 100000) ? 'logarithmic' : 'linear', grid:{color:'#1a1a24'}, ticks:{font:{size:9}}},
|
||
x: { grid: {display: false}, ticks:{font:{size:9},maxRotation:35}}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// Chart 2 : donut of distribution if applicable
|
||
const c2 = document.getElementById(`dd-${modId}-chart-2`);
|
||
if (c2 && numericKpis.length > 1) {
|
||
const k2 = `dd-${modId}-chart-2`;
|
||
if (CHARTS[k2]) CHARTS[k2].destroy();
|
||
|
||
// For specific modules, custom donut data
|
||
let labels, data;
|
||
if (modId === 'marketing') {
|
||
labels = ['DZ','MA','TN','INTL'];
|
||
data = [modData.ethica_dz||0, modData.ethica_ma||0, modData.ethica_tn||0, (modData.ethica_hcps_total||0) - (modData.ethica_dz||0) - (modData.ethica_ma||0) - (modData.ethica_tn||0)];
|
||
} else if (modId === 'supply') {
|
||
labels = ['Active','Suspended','Other'];
|
||
data = [modData.office_accounts_active||0, modData.office_accounts_suspended||0, (modData.office_accounts_total||0) - (modData.office_accounts_active||0) - (modData.office_accounts_suspended||0)];
|
||
} else if (modId === 'quality') {
|
||
labels = ['NonReg','L99','7σ'];
|
||
data = [modData.nonreg_pass||0, modData.l99_pass||0, modData.seven_sigma_pass||0];
|
||
} else {
|
||
labels = numericKpis.slice(0,5);
|
||
data = labels.map(k => modData[k]||0);
|
||
}
|
||
|
||
CHARTS[k2] = new Chart(c2, {
|
||
type: 'doughnut',
|
||
data: {
|
||
labels,
|
||
datasets: [{
|
||
data,
|
||
backgroundColor: COLORS.slice(0, labels.length),
|
||
borderWidth: 1, borderColor: '#0c0c12'
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true, maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: {position: 'right', labels: {boxWidth: 9, padding: 6, font:{size: 9}}},
|
||
title: {display:true, text:'Distribution', font:{size:10}}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}, 50);
|
||
}
|
||
|
||
loadAll();
|
||
setInterval(loadAll, 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) {
|
||
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 (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);} });
|
||
}
|
||
}
|
||
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 t33b6) --><script src="/wtp-unified-dock.js" defer></script>
|
||
<script src="/opus-antioverlap-doctrine.js?v=1776776094" defer></script>
|
||
</body>
|
||
</html>
|