Files
html/wevia-director-dashboard.html
opus d5edaa769c
Some checks failed
WEVAL NonReg / nonreg (push) Has been cancelled
auto-sync via WEVIA git_sync_all intent 2026-04-21T14:56:43+02:00
2026-04-21 14:56:43 +02:00

533 lines
27 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/","/login.html","/register","/weval-login.html","/weval-login","/arsenal-login.html","/arsenal-login","/ethica-login.html","/ethica-login","/office-login.html","/office-login","/wtp-login.html","/wtp-login","/azure-reregister.html","/azure-reregister","/authentik-callback","/auth-callback","/reset-password.html","/reset-password","/signup.html","/signup"];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>
<!-- WAVE 162 — Unified Pipeline Overlay -->
<div id="unifiedLiveOverlay" style="position:fixed;bottom:12px;right:12px;width:280px;max-height:calc(100vh - 120px);overflow-y:auto;background:linear-gradient(135deg,rgba(10,14,26,0.94),rgba(30,30,60,0.92));border:1px solid rgba(6,182,212,0.4);border-radius:10px;padding:10px;backdrop-filter:blur(14px);z-index:9999;font:600 9px Nunito,system-ui;color:#e2e8f0;box-shadow:0 4px 30px rgba(0,0,0,0.5)">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;padding-bottom:5px;border-bottom:1px solid rgba(100,116,139,0.3)">
<div style="font:900 10px Orbitron,system-ui;color:#06b6d4">🔴 <b id=closeLive style=cursor:pointer;margin-right:6px;color:gray onclick=unifiedLiveOverlay.remove()>x</b>UNIFIED LIVE</div>
<div id="ulo-ts" style="font-size:8px;color:#64748b"></div>
</div>
<div id="ulo-body">Loading...</div>
</div>
<script>
(function(){
const U='/api/weval-unified-pipeline.php';
async function tick(){
try{
const r=await fetch(U,{cache:'no-cache'});
if(!r.ok) return;
/* 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}}}}
const body=document.getElementById('ulo-body');
const ts=document.getElementById('ulo-ts');
if(!body) return;
const h=d.l99.health||'?';
const hc={GREEN:'#10b981',YELLOW:'#f59e0b',RED:'#ef4444'}[h]||'#64748b';
let html='<div style="background:'+hc+'15;border-left:3px solid '+hc+';padding:5px;margin-bottom:5px;border-radius:3px"><b style="color:'+hc+'">● '+h+'</b> L99 <b>'+d.l99.pass+'/'+d.l99.total+'</b><br><span style="color:#94a3b8">Disk '+d.system.disk_pct+'% Docker '+d.system.docker_count+' Crons '+d.system.cron_count+'</span></div>';
html+='<div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;margin-bottom:5px"><div style="background:rgba(6,182,212,0.1);border:1px solid rgba(6,182,212,0.3);border-radius:4px;padding:4px"><div style="font:800 8px Orbitron;color:#06b6d4">SOVEREIGN</div><b>'+d.providers.count+'</b> providers<br><b>'+d.ollama.models+'</b> Ollama<br><b>'+d.qdrant.collections.length+'</b> Qdrant</div><div style="background:rgba(139,92,246,0.1);border:1px solid rgba(139,92,246,0.3);border-radius:4px;padding:4px"><div style="font:800 8px Orbitron;color:#8b5cf6">PAPERCLIP</div><b>'+d.goals.length+'</b> goals<br><b>'+d.projects.length+'</b> projects<br><b>'+d.routines.length+'</b> routines</div></div>';
html+='<div style="background:rgba(245,158,11,0.1);border:1px solid rgba(245,158,11,0.3);border-radius:4px;padding:4px;margin-bottom:5px"><div style="font:800 8px Orbitron;color:#f59e0b">ETHICA</div><b>'+(d.ethica.hcps_validated/1000).toFixed(0)+'K</b> HCPs '+d.ethica.coverage.join(' ')+'</div>';
const rpa=d.routines_per_agent||{};
const top=Object.entries(rpa).sort((a,b)=>b[1]-a[1]).slice(0,5);
if(top.length){
html+='<div style="font:800 8px Orbitron;color:#10b981;margin:4px 0">TOP AGENTS</div>';
top.forEach(([n,c])=>{html+='<div style="display:flex;justify-content:space-between;padding:1px 3px;background:rgba(16,185,129,0.05);border-radius:2px;margin-bottom:1px"><span>'+n+'</span><b style="color:#10b981">'+c+'</b></div>';});
}
html+='<div style="margin-top:5px;padding-top:4px;border-top:1px solid rgba(100,116,139,0.3);font-size:8px;color:#64748b;text-align:center"><a href="/wevia-master.html" style="color:#06b6d4">Master</a> · <a href="/agents-archi.html" style="color:#06b6d4">Archi</a> · <a href="/wevia-meeting-rooms.html" style="color:#06b6d4">Rooms</a> · <a href="https://paperclip.weval-consulting.com" style="color:#06b6d4" target="_blank">Paperclip</a></div>';
body.innerHTML=html;
if(ts) ts.textContent=new Date().toLocaleTimeString('fr-FR',{hour:'2-digit',minute:'2-digit',second:'2-digit'});
}catch(e){}
}
setTimeout(tick,1500);setInterval(tick,30000);
})();
</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 === -->
<script src="/api/archi-meta-badge.js" defer></script>
<script src="/api/a11y-auto-enhancer.js" defer></script>
<!-- WTP_UDOCK_V1 (Opus 21-avr t33b5) --><script src="/wtp-unified-dock.js" defer></script>
<script src="/opus-antioverlap-doctrine.js?v=1776776094" defer></script>
</body>
</html>