488 lines
23 KiB
HTML
488 lines
23 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>WEVIA Director — Autonomous Dashboard</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;700&family=Space+Grotesk:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root {
|
|
--bg: #0a0a0f; --bg2: #12121a; --bg3: #1a1a28;
|
|
--t1: #e8e8f0; --t2: #9090a8; --t3: #606078;
|
|
--accent: #00e5a0; --accent2: #00c7ff; --warn: #ffaa00; --crit: #ff4060;
|
|
--ok: #00e5a0; --border: rgba(255,255,255,0.06);
|
|
}
|
|
* { margin:0; padding:0; box-sizing:border-box; }
|
|
body {
|
|
font-family: 'Space Grotesk', sans-serif;
|
|
background: var(--bg); color: var(--t1);
|
|
min-height: 100vh;
|
|
}
|
|
|
|
/* ── HEADER ── */
|
|
.hdr {
|
|
background: linear-gradient(135deg, var(--bg2), var(--bg3));
|
|
border-bottom: 1px solid var(--border);
|
|
padding: 20px 32px; display: flex; align-items: center; gap: 20px;
|
|
}
|
|
.hdr-logo {
|
|
width: 44px; height: 44px; border-radius: 12px;
|
|
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 20px; font-weight: 700; color: var(--bg);
|
|
}
|
|
.hdr-info h1 { font-size: 18px; font-weight: 600; letter-spacing: -0.3px; }
|
|
.hdr-info p { font-size: 12px; color: var(--t2); margin-top: 2px; font-family: 'JetBrains Mono', monospace; }
|
|
.hdr-right { margin-left: auto; display: flex; gap: 12px; align-items: center; }
|
|
.hdr-badge {
|
|
padding: 4px 12px; border-radius: 20px; font-size: 11px; font-weight: 600;
|
|
font-family: 'JetBrains Mono', monospace; letter-spacing: 0.5px;
|
|
}
|
|
.badge-live { background: rgba(0,229,160,0.15); color: var(--ok); border: 1px solid rgba(0,229,160,0.3); }
|
|
.badge-ver { background: rgba(0,199,255,0.1); color: var(--accent2); border: 1px solid rgba(0,199,255,0.2); }
|
|
.btn {
|
|
padding: 8px 16px; border-radius: 8px; border: 1px solid var(--border);
|
|
background: var(--bg3); color: var(--t1); cursor: pointer;
|
|
font-family: 'JetBrains Mono', monospace; font-size: 12px;
|
|
transition: all 0.2s;
|
|
}
|
|
.btn:hover { background: var(--accent); color: var(--bg); border-color: var(--accent); }
|
|
.btn-crit { border-color: var(--crit); color: var(--crit); }
|
|
.btn-crit:hover { background: var(--crit); color: white; }
|
|
|
|
/* ── GRID ── */
|
|
.grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; padding: 24px 32px; }
|
|
.grid-full { grid-column: 1 / -1; }
|
|
.grid-2 { grid-column: span 2; }
|
|
|
|
/* ── CARDS ── */
|
|
.card {
|
|
background: var(--bg2); border: 1px solid var(--border); border-radius: 12px;
|
|
padding: 20px; transition: border-color 0.3s;
|
|
}
|
|
.card:hover { border-color: rgba(0,229,160,0.2); }
|
|
.card-title {
|
|
font-size: 11px; font-weight: 600; color: var(--t2); text-transform: uppercase;
|
|
letter-spacing: 1px; margin-bottom: 12px;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
}
|
|
|
|
/* ── STATS ROW ── */
|
|
.stats { display: grid; grid-template-columns: repeat(6, 1fr); gap: 12px; }
|
|
.stat-card {
|
|
background: var(--bg2); border: 1px solid var(--border); border-radius: 10px;
|
|
padding: 16px; text-align: center;
|
|
}
|
|
.stat-val { font-size: 28px; font-weight: 700; font-family: 'JetBrains Mono', monospace; }
|
|
.stat-label { font-size: 10px; color: var(--t2); margin-top: 4px; text-transform: uppercase; letter-spacing: 1px; }
|
|
.val-ok { color: var(--ok); }
|
|
.val-warn { color: var(--warn); }
|
|
.val-crit { color: var(--crit); }
|
|
.val-info { color: var(--accent2); }
|
|
|
|
/* ── TIMELINE ── */
|
|
.timeline { max-height: 500px; overflow-y: auto; }
|
|
.tl-entry {
|
|
display: flex; gap: 14px; padding: 12px 0;
|
|
border-bottom: 1px solid var(--border);
|
|
animation: fadeIn 0.3s ease;
|
|
}
|
|
@keyframes fadeIn { from { opacity:0; transform:translateY(8px); } to { opacity:1; transform:none; } }
|
|
.tl-dot {
|
|
width: 10px; height: 10px; border-radius: 50%; margin-top: 5px; flex-shrink: 0;
|
|
}
|
|
.tl-dot.ok { background: var(--ok); box-shadow: 0 0 8px rgba(0,229,160,0.4); }
|
|
.tl-dot.warn { background: var(--warn); box-shadow: 0 0 8px rgba(255,170,0,0.4); }
|
|
.tl-dot.crit { background: var(--crit); box-shadow: 0 0 8px rgba(255,64,96,0.4); }
|
|
.tl-dot.info { background: var(--accent2); box-shadow: 0 0 8px rgba(0,199,255,0.4); }
|
|
.tl-time { font-size: 11px; color: var(--t3); font-family: 'JetBrains Mono', monospace; min-width: 60px; }
|
|
.tl-text { font-size: 13px; color: var(--t1); }
|
|
.tl-detail { font-size: 11px; color: var(--t2); margin-top: 4px; }
|
|
|
|
/* ── SERVERS ── */
|
|
.srv-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
|
.srv {
|
|
background: var(--bg3); border-radius: 8px; padding: 12px;
|
|
display: flex; align-items: center; gap: 10px;
|
|
border: 1px solid transparent; transition: border-color 0.3s;
|
|
}
|
|
.srv.alive { border-color: rgba(0,229,160,0.2); }
|
|
.srv.down { border-color: rgba(255,64,96,0.3); }
|
|
.srv-dot { width: 8px; height: 8px; border-radius: 50%; }
|
|
.srv-dot.on { background: var(--ok); box-shadow: 0 0 6px rgba(0,229,160,0.5); }
|
|
.srv-dot.off { background: var(--crit); box-shadow: 0 0 6px rgba(255,64,96,0.5); }
|
|
.srv-name { font-size: 13px; font-weight: 500; }
|
|
.srv-meta { font-size: 10px; color: var(--t3); font-family: 'JetBrains Mono', monospace; }
|
|
.srv-disk { margin-left: auto; font-size: 12px; font-family: 'JetBrains Mono', monospace; }
|
|
|
|
/* ── AGENTS ── */
|
|
.agent-list { display: flex; flex-direction: column; gap: 8px; }
|
|
.agent-row {
|
|
display: flex; align-items: center; gap: 10px; padding: 8px 12px;
|
|
background: var(--bg3); border-radius: 8px; font-size: 13px;
|
|
}
|
|
.agent-icon { font-size: 16px; }
|
|
.agent-name { font-weight: 500; }
|
|
.agent-runs { margin-left: auto; font-size: 11px; color: var(--t3); font-family: 'JetBrains Mono', monospace; }
|
|
|
|
/* ── LOG VIEWER ── */
|
|
.log-view {
|
|
background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
|
|
padding: 12px; font-family: 'JetBrains Mono', monospace; font-size: 11px;
|
|
color: var(--t2); max-height: 300px; overflow-y: auto; white-space: pre-wrap;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
/* ── PULSE ── */
|
|
.pulse { animation: pulse 2s infinite; }
|
|
@keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:0.5; } }
|
|
|
|
/* ── SCROLLBAR ── */
|
|
::-webkit-scrollbar { width: 6px; }
|
|
::-webkit-scrollbar-track { background: transparent; }
|
|
::-webkit-scrollbar-thumb { background: var(--t3); border-radius: 3px; }
|
|
::-webkit-scrollbar-thumb:hover { background: var(--t2); }
|
|
|
|
@media (max-width: 900px) {
|
|
.grid { grid-template-columns: 1fr; }
|
|
.grid-2 { grid-column: span 1; }
|
|
.stats { grid-template-columns: repeat(3, 1fr); }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- HEADER -->
|
|
<div class="hdr">
|
|
<div class="hdr-logo">W</div>
|
|
<div class="hdr-info">
|
|
<h1>WEVIA Director</h1>
|
|
<p>Autonomous Project Director — Observe · Plan · Act · Verify</p>
|
|
</div>
|
|
<div class="hdr-right">
|
|
<span class="hdr-badge badge-ver">v1.0</span>
|
|
<span class="hdr-badge badge-live pulse" id="liveStatus">● LOADING</span>
|
|
<button class="btn" onclick="triggerCycle()">▶ Run Cycle</button>
|
|
<button class="btn btn-crit" onclick="triggerCycle(true)">⚡ Force</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- STATS ROW -->
|
|
<div style="padding: 24px 32px 0;">
|
|
<div class="stats">
|
|
<div class="stat-card"><div class="stat-val val-ok" id="stCycles">—</div><div class="stat-label">Cycles</div></div>
|
|
<div class="stat-card"><div class="stat-val val-info" id="stObs">—</div><div class="stat-label">Observations</div></div>
|
|
<div class="stat-card"><div class="stat-val val-warn" id="stIssues">—</div><div class="stat-label">Issues</div></div>
|
|
<div class="stat-card"><div class="stat-val val-ok" id="stActions">—</div><div class="stat-label">Actions</div></div>
|
|
<div class="stat-card"><div class="stat-val val-crit" id="stEscalations">—</div><div class="stat-label">Escalations</div></div>
|
|
<div class="stat-card"><div class="stat-val val-info" id="stUptime">—</div><div class="stat-label">Uptime</div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- MAIN GRID -->
|
|
<div class="grid">
|
|
|
|
<!-- TIMELINE -->
|
|
<div class="card grid-2">
|
|
<div class="card-title">📋 Timeline — Ce que WEVIA a fait</div>
|
|
<div class="timeline" id="timeline">
|
|
<div style="color:var(--t3);text-align:center;padding:40px;">Chargement...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- SERVERS -->
|
|
<div class="card">
|
|
<div class="card-title">🖥 Serveurs</div>
|
|
<div class="srv-grid" id="servers">
|
|
<div class="srv"><div class="srv-dot"></div><div><div class="srv-name">Loading...</div></div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- AGENTS -->
|
|
<div class="card">
|
|
<div class="card-title">🤖 Agents actifs</div>
|
|
<div class="agent-list" id="agents">
|
|
<div class="agent-row"><span class="agent-icon">⏳</span><span>Loading...</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- FIABILITY -->
|
|
<div class="card">
|
|
<div class="card-title">🔍 Fiability Score</div>
|
|
<div id="fiability" style="text-align:center;padding:20px;">
|
|
<div class="stat-val val-ok" id="fiaScore" style="font-size:48px;">—%</div>
|
|
<div style="margin-top:8px;font-size:12px;color:var(--t2)" id="fiaDetail">Chargement...</div>
|
|
<div style="margin-top:12px;display:flex;gap:6px;justify-content:center;flex-wrap:wrap" id="fiaUrls"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- LAST CYCLE -->
|
|
<div class="card grid-2">
|
|
<div class="card-title">🔄 Dernier Cycle</div>
|
|
<div class="log-view" id="lastCycle">Chargement...</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
const API = '/api/wevia-director.php';
|
|
const MASTER_API = '/api/wevia-master-api.php';
|
|
|
|
async function loadAll() {
|
|
await Promise.all([loadStatus(), loadHistory(), loadHealth(), loadFiability()]);
|
|
}
|
|
|
|
async function loadFiability() {
|
|
try {
|
|
const r = await fetch('/api/wevia-fiability.php?report');
|
|
/* 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.status === 'no_report') {
|
|
document.getElementById('fiaScore').textContent = '—';
|
|
document.getElementById('fiaDetail').textContent = 'Aucun scan';
|
|
return;
|
|
}
|
|
const score = d.score || 0;
|
|
const el = document.getElementById('fiaScore');
|
|
el.textContent = score + '%';
|
|
el.className = 'stat-val ' + (score >= 90 ? 'val-ok' : score >= 70 ? 'val-warn' : 'val-crit');
|
|
const s = d.summary || {};
|
|
document.getElementById('fiaDetail').textContent =
|
|
`${s.ok||0}/${s.total_urls||0} URLs · ${s.subdomains_ok||0}/${s.subdomains_total||0} subs · ${d.duration_ms||0}ms`;
|
|
// URL dots
|
|
const urlsEl = document.getElementById('fiaUrls');
|
|
urlsEl.innerHTML = (d.results || []).map(r => {
|
|
const color = r.status === 'ok' ? 'var(--ok)' : r.status === 'slow' ? 'var(--warn)' : 'var(--crit)';
|
|
return `<div title="${r.url} ${r.code} ${r.time_ms}ms" style="width:10px;height:10px;border-radius:50%;background:${color}"></div>`;
|
|
}).join('');
|
|
} catch(e) {}
|
|
}
|
|
|
|
async function loadStatus() {
|
|
try {
|
|
const r = await fetch(API + '?status');
|
|
/* 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.status === 'never_run') {
|
|
document.getElementById('liveStatus').textContent = '● NEVER RUN';
|
|
document.getElementById('liveStatus').className = 'hdr-badge badge-ver';
|
|
document.getElementById('lastCycle').textContent = 'Director has never run. Click "Run Cycle" to start.';
|
|
return;
|
|
}
|
|
|
|
document.getElementById('liveStatus').textContent = '● ACTIVE';
|
|
document.getElementById('liveStatus').className = 'hdr-badge badge-live pulse';
|
|
|
|
// Observations
|
|
const obs = d.observations || {};
|
|
updateServers(obs);
|
|
|
|
// Last cycle detail
|
|
const detail = [
|
|
`Phase: ${d.phase}`,
|
|
`Time: ${d.timestamp}`,
|
|
`Duration: ${d.duration_ms}ms`,
|
|
`Report: ${d.report}`,
|
|
'',
|
|
'── Observations ──',
|
|
...Object.entries(obs).map(([k,v]) => ` ${k}: ${typeof v === 'object' ? JSON.stringify(v) : v}`),
|
|
];
|
|
if (d.plan && d.plan.length) {
|
|
detail.push('', '── Plan ──');
|
|
d.plan.forEach(p => detail.push(` [${p.severity}] ${p.name}: ${p.detail}`));
|
|
}
|
|
if (d.actions && d.actions.length) {
|
|
detail.push('', '── Actions ──');
|
|
d.actions.forEach(a => detail.push(` ${a.status === 'ok' ? '✅' : '❌'} ${a.task.name} (${a.result?.method || '?'})`));
|
|
}
|
|
if (d.escalations && d.escalations.length) {
|
|
detail.push('', '── Escalations ──');
|
|
d.escalations.forEach(e => detail.push(` 🚨 ${e.name}: ${e.detail}`));
|
|
}
|
|
document.getElementById('lastCycle').textContent = detail.join('\n');
|
|
|
|
// Stats
|
|
document.getElementById('stObs').textContent = Object.keys(obs).length;
|
|
document.getElementById('stIssues').textContent = (d.plan || []).length;
|
|
document.getElementById('stActions').textContent = (d.actions || []).length;
|
|
document.getElementById('stEscalations').textContent = (d.escalations || []).length;
|
|
} catch(e) {
|
|
document.getElementById('liveStatus').textContent = '● OFFLINE';
|
|
document.getElementById('liveStatus').className = 'hdr-badge';
|
|
document.getElementById('liveStatus').style.cssText = 'background:rgba(255,64,96,0.15);color:var(--crit);border:1px solid rgba(255,64,96,0.3)';
|
|
}
|
|
}
|
|
|
|
async function loadHistory() {
|
|
try {
|
|
const r = await fetch(API + '?history&n=30');
|
|
/* HTML_GUARD_V2_BATCH */ const _t_data=await r.text(); const data=null; {var _q=(_t_data||"").trim();if(_q.startsWith("<!DOCTYPE")||_q.startsWith("<html")){data={error:"[HTTP "+(r.status||"?")+"] Backend indisponible",isHtmlError:true};}else{try{data=JSON.parse(_q)}catch(e){data={error:"[JSON] "+e.message}}}}
|
|
if (!Array.isArray(data) || !data.length) {
|
|
document.getElementById('timeline').innerHTML = '<div style="color:var(--t3);text-align:center;padding:40px;">Aucun historique. Lancez un cycle.</div>';
|
|
document.getElementById('stCycles').textContent = '0';
|
|
return;
|
|
}
|
|
|
|
document.getElementById('stCycles').textContent = data.length;
|
|
|
|
const html = data.reverse().map(entry => {
|
|
const sev = entry.escalations > 0 ? 'crit' : entry.issues > 0 ? 'warn' : 'ok';
|
|
const time = entry.ts ? new Date(entry.ts).toLocaleTimeString('fr-FR', {hour:'2-digit',minute:'2-digit'}) : '??:??';
|
|
const date = entry.ts ? new Date(entry.ts).toLocaleDateString('fr-FR',{day:'2-digit',month:'short'}) : '';
|
|
return `<div class="tl-entry">
|
|
<div class="tl-dot ${sev}"></div>
|
|
<div class="tl-time">${time}<br>${date}</div>
|
|
<div>
|
|
<div class="tl-text">${entry.report || 'Cycle completed'}</div>
|
|
<div class="tl-detail">Actions: ${entry.actions || 0} | Issues: ${entry.issues || 0} | ${entry.duration_ms || 0}ms</div>
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
document.getElementById('timeline').innerHTML = html;
|
|
} catch(e) {
|
|
document.getElementById('timeline').innerHTML = '<div style="color:var(--crit);padding:20px;">Error loading history</div>';
|
|
}
|
|
}
|
|
|
|
async function loadHealth() {
|
|
try {
|
|
const r = await fetch(API + '?health');
|
|
/* 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}}}}
|
|
document.getElementById('stUptime').textContent = d.uptime || '?';
|
|
} catch(e) {}
|
|
|
|
// Agents status
|
|
try {
|
|
const r2 = await fetch(MASTER_API + '?health');
|
|
/* HTML_GUARD_V2_BATCH */ const _t_h=await r2.text(); const h=null; {var _q=(_t_h||"").trim();if(_q.startsWith("<!DOCTYPE")||_q.startsWith("<html")){h={error:"[HTTP "+(r2.status||"?")+"] Backend indisponible",isHtmlError:true};}else{try{h=JSON.parse(_q)}catch(e){h={error:"[JSON] "+e.message}}}}
|
|
updateAgents(h);
|
|
} catch(e) {}
|
|
}
|
|
|
|
function updateServers(obs) {
|
|
const servers = [
|
|
{ name: 'S204', ip: '204.168.152.13', role: 'PRIMARY · WEVIA · Ethica', disk: obs.s204_disk?.percent, alive: true },
|
|
{ name: 'S95', ip: '95.216.167.89', role: 'WEVADS · Arsenal · Sentinel', disk: obs.s95_disk?.percent, alive: (obs.s95_alive||'').includes('ALIVE') },
|
|
{ name: 'S151', ip: '151.80.235.110', role: 'OVH Tracking', disk: null, alive: obs.s151_http === '200' },
|
|
{ name: 'S88', ip: '—', role: 'DECOMMISSIONED', disk: null, alive: false },
|
|
];
|
|
|
|
document.getElementById('servers').innerHTML = servers.map(s => {
|
|
const diskClass = s.disk > 90 ? 'val-crit' : s.disk > 80 ? 'val-warn' : 'val-ok';
|
|
return `<div class="srv ${s.alive ? 'alive' : 'down'}">
|
|
<div class="srv-dot ${s.alive ? 'on' : 'off'}"></div>
|
|
<div>
|
|
<div class="srv-name">${s.name}</div>
|
|
<div class="srv-meta">${s.role}</div>
|
|
</div>
|
|
${s.disk !== null && s.disk !== undefined ? `<div class="srv-disk ${diskClass}">${s.disk}%</div>` : ''}
|
|
</div>`;
|
|
}).join('');
|
|
}
|
|
|
|
function updateAgents(health) {
|
|
const agents = [
|
|
{ icon: '🎯', name: 'Director', desc: 'Project autonomy' },
|
|
{ icon: '🔧', name: 'DevOps', desc: 'Infrastructure monitoring' },
|
|
{ icon: '💊', name: 'Ethica', desc: 'HCP data management' },
|
|
{ icon: '🛡️', name: 'Security', desc: 'Threat detection' },
|
|
{ icon: '📊', name: 'Monitor', desc: 'System health' },
|
|
];
|
|
document.getElementById('agents').innerHTML = agents.map(a =>
|
|
`<div class="agent-row">
|
|
<span class="agent-icon">${a.icon}</span>
|
|
<span class="agent-name">${a.name}</span>
|
|
<span style="color:var(--t3);font-size:11px">${a.desc}</span>
|
|
<span class="agent-runs">READY</span>
|
|
</div>`
|
|
).join('');
|
|
}
|
|
|
|
async function triggerCycle(force = false) {
|
|
const btn = event.target;
|
|
btn.textContent = '⏳ Running...';
|
|
btn.disabled = true;
|
|
try {
|
|
const url = force ? API + '?run&force=1' : API + '?run';
|
|
const r = await fetch(url);
|
|
/* 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}}}}
|
|
btn.textContent = d.skipped ? '⏭ Skipped' : '✅ Done';
|
|
await loadAll();
|
|
} catch(e) {
|
|
btn.textContent = '❌ Error';
|
|
}
|
|
setTimeout(() => { btn.textContent = force ? '⚡ Force' : '▶ Run Cycle'; btn.disabled = false; }, 3000);
|
|
}
|
|
|
|
// Initial load + auto-refresh
|
|
loadAll();
|
|
setInterval(loadAll, 60000);
|
|
</script>
|
|
<script>(function(){if(document.getElementById("weval-gl"))return;var p=window.location.pathname;var pub=["/","/index.html","/wevia.html","/wevia-widget.html","/enterprise-model.html","/wevia","/login","/api/"];var isPub=pub.some(function(x){return p===x||p.indexOf("/api/")===0;});if(isPub)return;var a=document.createElement("a");a.id="weval-gl";a.href="/logout";a.textContent="Logout";a.style.cssText="position:fixed;top:10px;right:12px;z-index:99990;padding:5px 10px;background:rgba(30,30,50,0.7);color:rgba(200,210,230,0.8);border:1px solid rgba(100,100,140,0.3);border-radius:6px;font:500 11px system-ui,sans-serif;text-decoration:none;opacity:0.6;cursor:pointer;backdrop-filter:blur(6px);transition:all .15s";a.onmouseover=function(){this.style.opacity="1";this.style.background="rgba(239,68,68,0.85)";this.style.color="white"};a.onmouseout=function(){this.style.opacity="0.6";this.style.background="rgba(30,30,50,0.7)";this.style.color="rgba(200,210,230,0.8)"};document.body.appendChild(a)})()</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 === -->
|
|
|
|
</body>
|
|
</html>
|