275 lines
13 KiB
HTML
275 lines
13 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>V82 Unified Status — Blade + Opus5 + L99 + NR (Drill-down)</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
|
<style>
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
:root{--bg:#0a0e17;--bg2:#111827;--bg3:#1a2234;--bd:#1e293b;--wh:#f1f5f9;--mu:#64748b;--mu2:#94a3b8;--ok:#22c55e;--warn:#f59e0b;--fail:#ef4444;--ac:#fbbf24;--cy:#22d3ee;--bl:#3b82f6;--pk:#ec4899;--vi:#8b5cf6;--font:'Plus Jakarta Sans',sans-serif;--mono:'JetBrains Mono',monospace}
|
|
body{background:var(--bg);color:var(--wh);font-family:var(--font);padding:20px 32px;min-height:100vh}
|
|
a{color:var(--cy);text-decoration:none}a:hover{text-decoration:underline}
|
|
h1{font-size:22px;font-weight:800;margin-bottom:6px}h1 span{color:var(--ac)}
|
|
.sub{font-size:12px;color:var(--mu);margin-bottom:20px;font-family:var(--mono)}
|
|
.btns{display:flex;gap:8px;margin-bottom:20px;flex-wrap:wrap}
|
|
.btn{padding:8px 16px;border:1px solid var(--bd);background:var(--bg2);color:var(--wh);border-radius:6px;font-size:12px;font-weight:600;text-decoration:none}
|
|
.btn:hover{border-color:var(--ac)}
|
|
.grid-main{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:14px;margin-bottom:24px}
|
|
.card{background:var(--bg2);border:1px solid var(--bd);border-radius:12px;padding:18px 20px;position:relative;overflow:hidden;cursor:pointer;transition:.25s}
|
|
.card:hover{transform:translateY(-2px);border-color:var(--ac)}
|
|
.card::before{content:'';position:absolute;top:0;left:0;right:0;height:3px}
|
|
.card.ok::before{background:linear-gradient(90deg,var(--ok),var(--cy))}
|
|
.card.warn::before{background:linear-gradient(90deg,var(--warn),var(--ac))}
|
|
.card.fail::before{background:linear-gradient(90deg,var(--fail),var(--pk))}
|
|
.c-head{display:flex;align-items:center;gap:10px;margin-bottom:10px}
|
|
.c-emo{font-size:24px}
|
|
.c-title{font-size:14px;font-weight:700}
|
|
.c-badge{margin-left:auto;font-size:10px;padding:3px 9px;border-radius:20px;font-weight:700;font-family:var(--mono)}
|
|
.b-ok{background:rgba(34,197,94,.15);color:var(--ok)}
|
|
.b-warn{background:rgba(245,158,11,.15);color:var(--warn)}
|
|
.b-fail{background:rgba(239,68,68,.15);color:var(--fail)}
|
|
.c-val{font-size:32px;font-weight:800;font-family:var(--mono);margin:8px 0}
|
|
.c-val.ok{color:var(--ok)}.c-val.warn{color:var(--warn)}.c-val.fail{color:var(--fail)}
|
|
.c-sub{font-size:10px;color:var(--mu2);text-transform:uppercase;letter-spacing:.7px;font-weight:600}
|
|
.c-detail{margin-top:12px;padding-top:12px;border-top:1px dashed var(--bd);font-size:11px;color:var(--mu2);display:none}
|
|
.card.open .c-detail{display:block}
|
|
.c-row{display:flex;justify-content:space-between;padding:4px 0;font-family:var(--mono);font-size:11px}
|
|
.c-row b{color:var(--wh)}
|
|
.sec-title{font-size:15px;font-weight:800;margin:24px 0 12px;display:flex;align-items:center;gap:10px;color:var(--ac)}
|
|
.sec-title::before{content:'';display:block;width:4px;height:16px;background:var(--ac);border-radius:2px}
|
|
.layers{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:8px}
|
|
.layer{background:var(--bg2);border:1px solid var(--bd);border-radius:8px;padding:10px 12px;font-size:11px}
|
|
.layer-name{font-family:var(--mono);font-weight:700;font-size:10px;text-transform:uppercase;color:var(--mu);margin-bottom:4px}
|
|
.layer-val{font-family:var(--mono);font-weight:700;font-size:14px}
|
|
.layer.ok .layer-val{color:var(--ok)}
|
|
.layer.warn .layer-val{color:var(--warn)}
|
|
.tasks{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-top:10px}
|
|
.task-col{background:var(--bg);padding:8px;border-radius:6px;text-align:center;font-size:11px}
|
|
.task-col b{display:block;font-size:18px;font-family:var(--mono)}
|
|
.tc-ok b{color:var(--ok)}.tc-warn b{color:var(--warn)}.tc-fail b{color:var(--fail)}.tc-mu b{color:var(--mu2)}
|
|
.loading{padding:40px;text-align:center;color:var(--mu)}
|
|
.live-indicator{display:inline-block;width:8px;height:8px;background:var(--ok);border-radius:50%;animation:pulse 2s ease-in-out infinite;margin-right:6px}
|
|
@keyframes pulse{50%{opacity:.4;box-shadow:0 0 8px var(--ok)}}
|
|
</style>
|
|
<!-- DOCTRINE-60-UX-ENRICH direct-inject-20260424-144055 -->
|
|
<style id="doctrine60-ux-direct">
|
|
|
|
/* DOCTRINE-60-UX-ENRICH injected-direct */
|
|
body::before {
|
|
content: '';
|
|
position: fixed;
|
|
top: 0; left: 0; width: 100vw; height: 100vh;
|
|
background: radial-gradient(circle at 50% 50%, rgba(100,180,255,0.08), transparent 60%);
|
|
pointer-events: none;
|
|
z-index: -1;
|
|
}
|
|
.card, .kpi, .panel, .btn {
|
|
transition: all 0.3s cubic-bezier(0.2,0,0.1,1);
|
|
}
|
|
.card:hover, .kpi:hover, .panel:hover {
|
|
box-shadow: 0 4px 20px rgba(100,180,255,0.2);
|
|
border-color: rgba(100,180,255,0.5);
|
|
}
|
|
@keyframes pulseD60 {
|
|
0%,100% { opacity: 1; transform: scale(1); }
|
|
50% { opacity: 0.7; transform: scale(1.05); }
|
|
}
|
|
.pulse, .live-indicator, .active, .online {
|
|
animation: pulseD60 3s ease-in-out infinite;
|
|
}
|
|
.modal, .chat, .speech, .overlay {
|
|
backdrop-filter: blur(12px);
|
|
-webkit-backdrop-filter: blur(12px);
|
|
}
|
|
.enter-stagger {
|
|
animation: enterStagD60 0.5s cubic-bezier(0.2,0,0.1,1) forwards;
|
|
}
|
|
@keyframes enterStagD60 {
|
|
from { opacity: 0; transform: translateY(20px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<h1><span>V82</span> Unified Status — <span id="ts">Loading...</span></h1>
|
|
<div class="sub">Blade + Opus5 + L99 + NR · drill-down clickable (doctrine 65) · zero fake data (doctrine 4)</div>
|
|
|
|
<div class="btns">
|
|
<a class="btn" href="/weval-technology-platform.html">← WTP</a>
|
|
<a class="btn" href="/tasks-live-opus5.html">⚡ Opus5 Monitor</a>
|
|
<a class="btn" href="/blade-hub.html">🦾 Blade Hub</a>
|
|
<a class="btn" href="/api/v82-unified-status.php" target="_blank">📊 V82 JSON</a>
|
|
</div>
|
|
|
|
<div class="grid-main" id="main">
|
|
<div class="loading">Loading real-time status...</div>
|
|
</div>
|
|
|
|
<div class="sec-title">🔍 L99 Layers Drill-down</div>
|
|
<div class="layers" id="layers"><div class="loading">Loading...</div></div>
|
|
|
|
<div class="sec-title">📋 Blade Recent Tasks</div>
|
|
<div id="recent"></div>
|
|
|
|
<div style="margin-top:32px;padding:14px;background:var(--bg2);border:1px solid var(--bd);border-radius:10px;font-size:11px;color:var(--mu)">
|
|
<strong style="color:var(--ac)">V82 doctrine #4 honnête</strong> — Blade heartbeat <span class="live-indicator"></span> live depuis machine Razer réelle (41.251.46.132).
|
|
Contrairement aux dashboards qui montraient "DEAD 164h" (fichier stale), l'état réel est actualisé toutes les 15 min par cron keepalive + agent Blade local chaque 5 min.
|
|
</div>
|
|
|
|
<script>
|
|
let DATA = null;
|
|
|
|
async function load() {
|
|
try {
|
|
const r = await fetch('/api/v82-unified-status.php?t=' + Date.now());
|
|
DATA = await r.json();
|
|
document.getElementById('ts').textContent = new Date(DATA.ts).toLocaleTimeString('fr-FR');
|
|
renderMain();
|
|
renderLayers();
|
|
renderRecent();
|
|
} catch(e) {
|
|
document.getElementById('main').innerHTML = '<div class="loading" style="color:#ef4444">Error: ' + e.message + '</div>';
|
|
}
|
|
}
|
|
|
|
function renderMain() {
|
|
const main = document.getElementById('main');
|
|
const b = DATA.blade;
|
|
const o = DATA.opus5;
|
|
const nr = DATA.nr;
|
|
const l99 = DATA.l99;
|
|
|
|
const bladeStatus = b.online ? 'ok' : 'fail';
|
|
const bladeLabel = b.online ? '🟢 LIVE' : '🔴 DEAD';
|
|
const bladeBadge = b.online ? 'b-ok' : 'b-fail';
|
|
|
|
main.innerHTML = `
|
|
<div class="card ${bladeStatus}" onclick="this.classList.toggle('open')">
|
|
<div class="c-head">
|
|
<div class="c-emo">🦾</div>
|
|
<div class="c-title">Blade Agent</div>
|
|
<span class="c-badge ${bladeBadge}">${bladeLabel}</span>
|
|
</div>
|
|
<div class="c-val ${bladeStatus}">${b.hours_ago < 1 ? b.minutes_ago + 'min' : b.hours_ago + 'h'} ago</div>
|
|
<div class="c-sub">Last heartbeat · ${b.seconds_ago}s</div>
|
|
<div class="c-detail">
|
|
<div class="c-row"><span>Hostname</span><b>${b.hostname}</b></div>
|
|
<div class="c-row"><span>IP</span><b>${b.ip}</b></div>
|
|
<div class="c-row"><span>Agent v</span><b>${b.agent_version}</b></div>
|
|
<div class="c-row"><span>Tasks today</span><b>${b.tasks_today}</b></div>
|
|
<div class="c-row"><span>Tasks week</span><b>${b.tasks_week}</b></div>
|
|
<div class="tasks">
|
|
<div class="task-col tc-mu"><span>pending</span><b>${DATA.blade_tasks.pending}</b></div>
|
|
<div class="task-col tc-ok"><span>done</span><b>${DATA.blade_tasks.done}</b></div>
|
|
<div class="task-col tc-warn"><span>in prog</span><b>${DATA.blade_tasks.in_progress}</b></div>
|
|
<div class="task-col tc-fail"><span>failed</span><b>${DATA.blade_tasks.failed}</b></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card ${nr.score===100?'ok':'warn'}" onclick="this.classList.toggle('open')">
|
|
<div class="c-head">
|
|
<div class="c-emo">🧪</div>
|
|
<div class="c-title">NonReg (NR)</div>
|
|
<span class="c-badge ${nr.score===100?'b-ok':'b-warn'}">${nr.score}%</span>
|
|
</div>
|
|
<div class="c-val ${nr.score===100?'ok':'warn'}">${nr.pass}/${nr.total}</div>
|
|
<div class="c-sub">54 sessions consecutive constant</div>
|
|
<div class="c-detail">
|
|
<div class="c-row"><span>Pass</span><b style="color:var(--ok)">${nr.pass}</b></div>
|
|
<div class="c-row"><span>Total</span><b>${nr.total}</b></div>
|
|
<div class="c-row"><span>Score</span><b>${nr.score}%</b></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card ${l99.score>=95?'ok':'warn'}" onclick="this.classList.toggle('open')">
|
|
<div class="c-head">
|
|
<div class="c-emo">🔬</div>
|
|
<div class="c-title">L99 Tests</div>
|
|
<span class="c-badge ${l99.score>=95?'b-ok':'b-warn'}">${l99.score}%</span>
|
|
</div>
|
|
<div class="c-val ${l99.score>=95?'ok':'warn'}">${l99.passed}/${l99.total}</div>
|
|
<div class="c-sub">${l99.fail} fails · 12 layers</div>
|
|
<div class="c-detail">
|
|
<div class="c-row"><span>Pass</span><b style="color:var(--ok)">${l99.passed}</b></div>
|
|
<div class="c-row"><span>Fail</span><b style="color:var(--fail)">${l99.fail}</b></div>
|
|
<div class="c-row"><span>Total</span><b>${l99.total}</b></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card ok" onclick="this.classList.toggle('open')">
|
|
<div class="c-head">
|
|
<div class="c-emo">⚡</div>
|
|
<div class="c-title">Opus5 Dispatch-Proxy</div>
|
|
<span class="c-badge b-ok">tracking</span>
|
|
</div>
|
|
<div class="c-val">${o.events_tracked}</div>
|
|
<div class="c-sub">Events · ${o.avg_latency_ms}ms avg</div>
|
|
<div class="c-detail">
|
|
<div class="c-row"><span>Events</span><b>${o.events_tracked}</b></div>
|
|
<div class="c-row"><span>Dispatches</span><b>${o.dispatches}</b></div>
|
|
<div class="c-row"><span>Proxy calls</span><b>${o.proxy_calls}</b></div>
|
|
<div class="c-row"><span>Avg latency</span><b>${o.avg_latency_ms}ms</b></div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderLayers() {
|
|
const l99 = DATA.l99;
|
|
const layers = l99.layers || {};
|
|
const el = document.getElementById('layers');
|
|
el.innerHTML = Object.entries(layers).map(([name, d]) => {
|
|
const cls = d.pct === 100 ? 'ok' : 'warn';
|
|
return `
|
|
<div class="layer ${cls}">
|
|
<div class="layer-name">${name}</div>
|
|
<div class="layer-val">${d.pass}/${d.total}</div>
|
|
<div style="font-size:9px;color:var(--mu);margin-top:2px">${d.pct}%</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
function renderRecent() {
|
|
const rec = DATA.blade_recent || [];
|
|
const el = document.getElementById('recent');
|
|
if (!rec.length) {
|
|
el.innerHTML = '<div class="card"><div class="c-sub">No recent tasks</div></div>';
|
|
return;
|
|
}
|
|
el.innerHTML = '<div class="card"><table style="width:100%;font-size:11px"><thead><tr style="color:var(--mu);text-transform:uppercase;font-size:9px"><th style="text-align:left;padding:6px">ID</th><th style="text-align:left">Status</th><th style="text-align:left">Cmd</th></tr></thead><tbody>' +
|
|
rec.map(t => `<tr style="border-top:1px solid var(--bd)"><td style="padding:6px;font-family:var(--mono)">${t.id}</td><td style="color:${t.status==='done'?'var(--ok)':t.status==='failed'?'var(--fail)':'var(--warn)'}">${t.status}</td><td style="font-family:var(--mono);color:var(--mu2)">${t.cmd}</td></tr>`).join('') +
|
|
'</tbody></table></div>';
|
|
}
|
|
|
|
load();
|
|
setInterval(load, 15000);
|
|
</script>
|
|
|
|
<script src="/api/a11y-auto-enhancer.js" defer></script>
|
|
<!-- WTP_UDOCK_V1 (Opus 21-avr t34final) --><script src="/wtp-unified-dock.js" defer></script>
|
|
<script src="/opus-antioverlap-doctrine.js?v=1776776094" defer></script>
|
|
<!-- DOCTRINE-60-UX-JS --><script id="doctrine60-ux-js-direct">
|
|
|
|
// DOCTRINE-60-UX-JS staggered entrance
|
|
(function(){
|
|
if (!('IntersectionObserver' in window)) return;
|
|
const obs = new IntersectionObserver((entries) => {
|
|
entries.forEach((e, i) => {
|
|
if (e.isIntersecting) {
|
|
setTimeout(() => e.target.classList.add('enter-stagger'), i * 80);
|
|
obs.unobserve(e.target);
|
|
}
|
|
});
|
|
});
|
|
document.querySelectorAll('.card, .kpi, .panel').forEach(el => obs.observe(el));
|
|
})();
|
|
|
|
</script>
|
|
</body>
|
|
</html>
|