Files
html/blade-ai.html

675 lines
44 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8"><meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>WEVAL — Blade AI Controller</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=DM+Sans:wght@300;400;500;700&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{--bg:#06090f;--s1:#0d1321;--s2:#141d2f;--s3:#1a2744;--brd:#1e3054;--t1:#e8ecf4;--t2:#8899b4;--t3:#506380;--green:#00e09e;--red:#ff4d6a;--amber:#ffb547;--blue:#4da6ff;--purple:#a78bfa;--cyan:#22d3ee;--pink:#f472b6;--font:'DM Sans',sans-serif;--mono:'JetBrains Mono',monospace}
body{background:var(--bg);color:var(--t1);font-family:var(--font);overflow:hidden;height:100vh}
.app{display:grid;grid-template-columns:260px 1fr 320px;grid-template-rows:auto 48px 1fr;height:100vh}
/* SENTINEL BAR */
.sentinel-bar{grid-column:1/-1;background:#1a0a2e;border-bottom:1px solid #2d1b4e;padding:8px 20px;display:flex;align-items:center;gap:12px;font-size:12px}
.sentinel-bar .dot{width:8px;height:8px;border-radius:50%;background:var(--red);transition:background .3s}
.sentinel-bar .dot.on{background:var(--green);box-shadow:0 0 12px var(--green)}
/* TOPBAR */
.topbar{grid-column:1/-1;background:var(--s1);border-bottom:1px solid var(--brd);display:flex;align-items:center;padding:0 16px;gap:12px}
.topbar h1{font-size:15px;font-weight:700;letter-spacing:.5px}
.topbar .dot{width:8px;height:8px;border-radius:50%;background:var(--red);transition:background .3s}
.topbar .dot.on{background:var(--green);box-shadow:0 0 12px var(--green)}
@keyframes pulse{0%,100%{box-shadow:0 0 4px var(--green)}50%{box-shadow:0 0 16px var(--green),0 0 32px rgba(0,224,158,.2)}}
.topbar .dot.on{animation:pulse 2s ease infinite}
.topbar .status{font-size:11px;color:var(--t2);font-family:var(--mono)}
.topbar .tabs{margin-left:auto;display:flex;gap:2px}
.topbar .tab{padding:6px 14px;font-size:12px;background:transparent;border:none;color:var(--t2);cursor:pointer;border-radius:6px;font-family:var(--font)}
.topbar .tab.active{background:var(--s3);color:var(--t1)}
.topbar .tab:hover{color:var(--t1)}
/* SIDEBAR */
.sidebar{background:var(--s1);border-right:1px solid var(--brd);overflow-y:auto;padding:12px}
.sb-title{font-size:10px;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:1.5px;padding:4px 8px;margin-top:12px}
.sb-btn{width:100%;padding:8px 10px;background:transparent;border:1px solid transparent;color:var(--t2);border-radius:6px;cursor:pointer;font-size:12px;font-family:var(--font);text-align:left;display:flex;align-items:center;gap:8px;transition:all .12s}
.sb-btn:hover{background:var(--s2);color:var(--t1);border-color:var(--brd)}
.sb-btn:active{transform:scale(.97);opacity:.8}
.sb-btn .ico{font-size:14px;width:20px;text-align:center}
/* MAIN */
.main{overflow:hidden;display:flex;flex-direction:column}
.panel{display:none;flex:1;flex-direction:column;overflow-y:auto;padding:16px}
.panel.active{display:flex}
/* AI CHAT */
#panel-ai{display:flex;flex-direction:column;height:100%;padding:0}
.chat-messages{flex:1;overflow-y:auto;scroll-behavior:smooth;padding:16px;min-height:0}
.chat-msg{margin-bottom:12px;display:flex;gap:10px;animation:slideIn .25s ease}
@keyframes slideIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
.chat-msg .avatar{width:28px;height:28px;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;flex-shrink:0}
.chat-msg .avatar.ai{background:linear-gradient(135deg,#8b5cf6,#6366f1)}
.chat-msg .avatar.user{background:var(--s3);color:var(--blue)}
.chat-msg .bubble{background:var(--s2);border:1px solid var(--brd);border-radius:8px;padding:10px 12px;font-size:13px;line-height:1.6;max-width:90%}
.chat-msg .bubble code{font-family:var(--mono);font-size:11px;background:var(--s1);padding:1px 4px;border-radius:3px}
.chat-msg .bubble .task-result{background:var(--s1);border:1px solid var(--brd);border-radius:6px;padding:8px;margin-top:8px;font-family:var(--mono);font-size:11px;color:var(--cyan);max-height:120px;overflow-y:auto;white-space:pre-wrap}
.chat-input{display:flex;gap:8px;padding:12px 16px;border-top:1px solid var(--brd);flex-shrink:0;background:var(--s1)}
.chat-input textarea{flex:1;background:var(--s2);border:1px solid var(--brd);color:var(--t1);padding:10px 14px;border-radius:8px;font-size:13px;font-family:var(--font);outline:none;resize:none;min-height:42px;max-height:80px;line-height:1.4}
.chat-input textarea:focus{border-color:var(--blue)}
.chat-input button{background:var(--blue);color:#fff;border:none;padding:10px 20px;border-radius:8px;cursor:pointer;font-weight:600;font-size:13px;white-space:nowrap;flex-shrink:0}
.chat-input button:hover{box-shadow:0 2px 12px rgba(77,166,255,.3);transform:translateY(-1px)}
.chat-input button:active{transform:scale(.95)}
/* CARDS */
.cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:10px;margin-bottom:16px}
.card{background:var(--s2);border:1px solid var(--brd);border-radius:10px;padding:14px;cursor:pointer;transition:all .15s}
.card:hover{border-color:var(--blue);box-shadow:0 4px 20px rgba(77,166,255,.1);transform:translateY(-1px)}
.card .card-ico{font-size:20px;margin-bottom:6px}
.card .card-title{font-size:13px;font-weight:600;margin-bottom:2px}
.card .card-desc{font-size:11px;color:var(--t2);line-height:1.4}
/* RECIPES */
.recipe{background:var(--s2);border:1px solid var(--brd);border-radius:8px;padding:12px;margin-bottom:8px;display:flex;align-items:center;gap:12px;cursor:pointer;transition:all .15s}
.recipe:hover{border-color:var(--purple);box-shadow:0 2px 16px rgba(167,139,250,.08)}
.recipe .r-ico{font-size:22px}
.recipe .r-info{flex:1}
.recipe .r-title{font-size:13px;font-weight:600}
.recipe .r-desc{font-size:11px;color:var(--t2)}
.recipe .r-go{background:var(--s3);border:none;color:var(--t1);padding:6px 12px;border-radius:6px;cursor:pointer;font-size:11px;font-weight:600}
.recipe .r-go:hover{background:var(--purple);color:#fff}
/* RIGHT PANEL */
.rpanel{background:var(--s1);border-left:1px solid var(--brd);overflow-y:auto;padding:12px}
.rp-title{font-size:10px;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:1.5px;margin-bottom:8px;margin-top:12px}
.rp-metric{display:flex;justify-content:space-between;padding:5px 4px;font-size:12px;border-radius:4px;transition:background .1s}
.rp-metric:hover{background:var(--s2)}
.rp-metric .v{font-family:var(--mono);font-weight:600}
.rp-bar{height:4px;background:var(--s3);border-radius:2px;margin:4px 0 8px;overflow:hidden}
.rp-bar .fill{height:100%;border-radius:2px;transition:width .5s}
.task-mini{background:var(--s2);border:1px solid var(--brd);border-radius:6px;padding:8px;margin-bottom:6px;font-size:11px;transition:all .2s}
.task-mini:hover{border-color:var(--blue)}
.task-mini .tm-status{display:inline-block;width:6px;height:6px;border-radius:50%;margin-right:4px}
.task-mini .tm-status.pending{background:var(--amber)}
.task-mini .tm-status.done{background:var(--green)}
.task-mini .tm-status.failed{background:var(--red)}
.rpanel a{color:var(--cyan);text-decoration:none;transition:color .15s}
.rpanel a:hover{color:var(--green)!important}
.loading{color:var(--t3);font-size:12px;padding:20px;text-align:center}
::-webkit-scrollbar{width:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--s3);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:var(--brd)}
@media(max-width:1024px){.app{grid-template-columns:1fr}.sidebar,.rpanel{display:none}}
</style>
<!-- DOCTRINE-60-UX-ENRICH direct-inject-20260424-143816 -->
<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>
<div class="app">
<!-- SENTINEL BAR -->
<div class="sentinel-bar">
<span class="dot" id="s-dot"></span>
<span id="s-text" style="color:#94a3b8;font-family:var(--mono);font-size:11px">Sentinel: checking...</span>
<a id="s-install" href="/downloads/install-sentinel.bat" style="display:none;margin-left:auto;background:#7c3aed;color:#fff;padding:4px 14px;border-radius:6px;text-decoration:none;font-weight:600;font-size:11px" download>Installer Sentinel</a>
</div>
<!-- TOPBAR -->
<div class="topbar">
<span class="dot" id="t-dot"></span>
<h1>BLADE AI</h1>
<span class="status" id="t-status">connecting...</span>
<div class="tabs">
<button class="tab active" onclick="showPanel('ai',this)">AI Chat</button>
<button class="tab" onclick="showPanel('actions',this)">Actions</button>
<button class="tab" onclick="showPanel('recipes',this)">Automations</button>
<button class="tab" onclick="showPanel('tasks',this)">Tasks</button>
<button class="tab" onclick="showPanel('files',this)">Files</button>
</div>
</div>
<!-- SIDEBAR -->
<div class="sidebar">
<div class="sb-title">System</div>
<button class="sb-btn" onclick="pushQ('sysinfo','System Info')"><span class="ico">💻</span>System Info</button>
<button class="sb-btn" onclick="pushQ('screenshot','Screenshot')"><span class="ico">📸</span>Screenshot</button>
<button class="sb-btn" onclick="pushQ('powershell','Top Procs','Get-Process|Sort CPU -Desc|Select -First 15 Name,CPU,WS|Format-Table')"><span class="ico">⚙️</span>Processes</button>
<button class="sb-btn" onclick="pushQ('powershell','Disk Space','Get-PSDrive C,D,E -EA 0|Select Name,@{N=&quot;Free(GB)&quot;;E={[math]::Round($_.Free/1GB)}},@{N=&quot;Used(GB)&quot;;E={[math]::Round($_.Used/1GB)}}|FT')"><span class="ico">💾</span>Disk Space</button>
<button class="sb-btn" onclick="pushQ('powershell','Network','Get-NetIPAddress -AddressFamily IPv4|Select IPAddress,InterfaceAlias|FT')"><span class="ico">🌐</span>Network IPs</button>
<button class="sb-btn" onclick="pushQ('powershell','WiFi','netsh wlan show interfaces|Select-String SSID,Signal,Speed')"><span class="ico">📶</span>WiFi Status</button>
<button class="sb-btn" onclick="pushQ('powershell','Battery','(Get-CimInstance Win32_Battery|Select EstimatedChargeRemaining,BatteryStatus)|FT')"><span class="ico">🔋</span>Battery</button>
<button class="sb-btn" onclick="pushQ('powershell','GPU Info','(Get-CimInstance Win32_VideoController|Select Name,AdapterRAM,DriverVersion)|FT')"><span class="ico">🎮</span>GPU Info</button>
<div class="sb-title">Apps & URLs</div>
<button class="sb-btn" onclick="pushQ('open_url','WEVAL Site','https://weval-consulting.com')"><span class="ico">🏠</span>WEVAL Site</button>
<button class="sb-btn" onclick="pushQ('open_url','Arsenal','https://weval-consulting.com/arsenal-proxy/ceo-dashboard.html')"><span class="ico">🎯</span>Arsenal CEO</button>
<button class="sb-btn" onclick="pushQ('open_url','WEVIA','https://weval-consulting.com/wevia')"><span class="ico">🤖</span>WEVIA Chat</button>
<button class="sb-btn" onclick="pushQ('open_url','Analytics','https://analytics.weval-consulting.com')"><span class="ico">📊</span>Plausible</button>
<button class="sb-btn" onclick="pushQ('open_url','CRM','https://crm.weval-consulting.com')"><span class="ico">👥</span>Twenty CRM</button>
<div class="sb-title">Git & Dev</div>
<button class="sb-btn" onclick="pushQ('git_pull','Git Pull')"><span class="ico">⬇️</span>Git Pull</button>
<button class="sb-btn" onclick="pushQ('git_push','Git Push')"><span class="ico">⬆️</span>Git Push</button>
<button class="sb-btn" onclick="pushQ('powershell','VS Code','code C:\\Users\\Yace\\Desktop\\CLAUDE\\weval-consulting')"><span class="ico">📝</span>Open VS Code</button>
</div>
<!-- MAIN -->
<div class="main">
<!-- AI CHAT -->
<div class="panel active" id="panel-ai">
<div class="chat-messages" id="chat-messages">
<div class="chat-msg"><div class="avatar ai">AI</div><div class="bubble">Blade AI Controller actif. Je suis l'IA de WEVAL — dis-moi ce que tu veux faire sur le Blade Razer en langage naturel.<br><br>Exemples :<br><code>Prends un screenshot</code><br><code>Ouvre le site WEVAL et le CRM</code><br><code>Montre-moi les gros processus</code><br><code>Installe Node.js</code><br><code>Nettoie les fichiers temp</code><br><code>Quel est l'espace disque?</code></div></div>
</div>
<div class="chat-input">
<textarea id="chat-in" placeholder="Dis ce que tu veux faire sur le Blade..." rows="1" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendChat()}"></textarea>
<button onclick="sendChat()">Envoyer</button>
</div>
</div>
<!-- ACTIONS -->
<div class="panel" id="panel-actions">
<h2 style="font-size:16px;margin-bottom:12px">Actions rapides</h2>
<div class="cards">
<div class="card" onclick="pushQ('powershell','Cleanup Temp','Remove-Item $env:TEMP\\* -Recurse -Force -EA 0;Write-Host Temp cleaned')"><div class="card-ico">🧹</div><div class="card-title">Nettoyer Temp</div><div class="card-desc">Supprime les fichiers temporaires</div></div>
<div class="card" onclick="pushQ('powershell','Installed Apps','Get-ItemProperty HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*|Select DisplayName,DisplayVersion|Sort DisplayName|FT -Auto')"><div class="card-ico">📦</div><div class="card-title">Apps installées</div><div class="card-desc">Liste tous les logiciels</div></div>
<div class="card" onclick="pushQ('powershell','Services','Get-Service|Where Status -eq Running|Select Name,DisplayName|Sort DisplayName|FT -Auto')"><div class="card-ico">🔧</div><div class="card-title">Services actifs</div><div class="card-desc">Services Windows en cours</div></div>
<div class="card" onclick="pushQ('powershell','Ports','Get-NetTCPConnection -State Listen|Select LocalPort,OwningProcess|Sort LocalPort|FT')"><div class="card-ico">🔌</div><div class="card-title">Ports ouverts</div><div class="card-desc">TCP Listen</div></div>
<div class="card" onclick="pushQ('powershell','IP Public','(Invoke-WebRequest -Uri https://api.ipify.org -UseBasicParsing).Content')"><div class="card-ico">🌍</div><div class="card-title">IP Publique</div><div class="card-desc">Adresse IP externe</div></div>
<div class="card" onclick="pushQ('powershell','Recent Files','Get-ChildItem $env:USERPROFILE\\Downloads,$env:USERPROFILE\\Desktop -File|Sort LastWriteTime -Desc|Select -First 20 Name,Length,LastWriteTime|FT')"><div class="card-ico">📄</div><div class="card-title">Fichiers récents</div><div class="card-desc">Downloads + Desktop</div></div>
</div>
</div>
<!-- AUTOMATIONS -->
<div class="panel" id="panel-recipes">
<h2 style="font-size:16px;margin-bottom:12px">Automations Blade</h2>
<div class="recipe" onclick="runRecipe('morning')"><div class="r-ico">🌅</div><div class="r-info"><div class="r-title">Routine matinale</div><div class="r-desc">Ouvre WEVAL, Arsenal, CRM, Analytics + Git Pull + Screenshot</div></div><button class="r-go">GO</button></div>
<div class="recipe" onclick="runRecipe('devsetup')"><div class="r-ico">💻</div><div class="r-info"><div class="r-title">Setup Dev</div><div class="r-desc">Git Pull + VS Code + Terminal + WEVADS</div></div><button class="r-go">GO</button></div>
<div class="recipe" onclick="runRecipe('cleanup')"><div class="r-ico">🧹</div><div class="r-info"><div class="r-title">Nettoyage complet</div><div class="r-desc">Temp + Downloads vieux + Cache navigateur</div></div><button class="r-go">GO</button></div>
<div class="recipe" onclick="runRecipe('security')"><div class="r-ico">🔒</div><div class="r-info"><div class="r-title">Audit sécurité</div><div class="r-desc">Firewall + Ports + Services + Updates</div></div><button class="r-go">GO</button></div>
<div class="recipe" onclick="runRecipe('endday')"><div class="r-ico">🌙</div><div class="r-info"><div class="r-title">Fin de journée</div><div class="r-desc">Git Push + Screenshot + Cleanup + Notification</div></div><button class="r-go">GO</button></div>
</div>
<!-- TASKS -->
<div class="panel" id="panel-tasks">
<h2 style="font-size:16px;margin-bottom:12px">Historique des tâches
<button onclick="loadTasks()" style="background:var(--s3);border:none;color:var(--t2);padding:4px 10px;border-radius:4px;cursor:pointer;font-size:11px;margin-left:8px">Reload</button>
<button onclick="clearTasks()" style="background:var(--s3);border:none;color:var(--t2);padding:4px 10px;border-radius:4px;cursor:pointer;font-size:11px">Clear</button></h2>
<div id="tasks-full" class="loading">Chargement...</div>
</div>
<!-- FILES -->
<div class="panel" id="panel-files">
<h2 style="font-size:16px;margin-bottom:12px">File Manager</h2>
<div style="display:flex;gap:8px;margin-bottom:12px">
<input type="text" id="file-path" value="C:\Users\Yace\Desktop" style="flex:1;background:var(--s2);border:1px solid var(--brd);color:var(--t1);padding:8px;border-radius:6px;font-family:var(--mono);font-size:12px">
<button onclick="browseDir()" style="background:var(--blue);color:#fff;border:none;padding:8px 14px;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600">Browse</button>
</div>
<div id="file-list" class="loading">Entrez un chemin et cliquez Browse</div>
</div>
</div>
<!-- RIGHT PANEL -->
<div class="rpanel">
<div class="rp-title">Blade Health</div>
<div class="rp-metric"><span>CPU</span><span class="v" id="r-cpu"></span></div>
<div class="rp-bar"><div class="fill" id="bar-cpu" style="width:0;background:var(--blue)"></div></div>
<div class="rp-metric"><span>RAM</span><span class="v" id="r-ram"></span></div>
<div class="rp-bar"><div class="fill" id="bar-ram" style="width:0;background:var(--purple)"></div></div>
<div class="rp-metric"><span>Disk</span><span class="v" id="r-disk"></span></div>
<div class="rp-bar"><div class="fill" id="bar-disk" style="width:0;background:var(--cyan)"></div></div>
<div class="rp-metric"><span>Uptime</span><span class="v" id="r-up"></span></div>
<div class="rp-metric"><span>User</span><span class="v" id="r-user"></span></div>
<div class="rp-metric"><span>Last seen</span><span class="v" id="r-ts"></span></div>
<div class="rp-title">Queue <span id="r-count" style="color:var(--amber)"></span></div>
<div id="r-tasks"></div>
<div class="rp-title">Liens rapides</div>
<div style="font-size:11px;line-height:2">
<a href="/blade-center.html">Blade Center</a><br>
<a href="/ops-center.html">WEVAL Manager</a><br>
<a href="/api/blade-api.php?action=status">Blade API JSON</a><br>
<a href="/products/weval-sentinel-agent.ps1">Agent v2.2</a><br>
<a href="/api/nonreg-report.html">NonReg Report</a>
</div>
</div>
</div>
<script>
// ============================================================
// BLADE AI — UNIFIED JAVASCRIPT (rebuilt 13-Apr-2026)
// Single source of truth — no duplicate pollers
// ============================================================
const API = '/api/blade-api.php', KEY = 'BLADE2026';
let _fails = 0; // grace counter for offline state
function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
async function api(p) {
p.k = KEY;
const body = Object.entries(p).map(([k,v]) => `${k}=${encodeURIComponent(v)}`).join('&');
const r = await fetch(API, { method: 'POST', headers: {'Content-Type':'application/x-www-form-urlencoded'}, body });
return r.json();
}
// === SINGLE STATUS CHECK — updates sentinel bar + topbar + health ===
async function checkStatus() {
try {
const c = new AbortController();
setTimeout(() => c.abort(), 8000);
const r = await fetch('/api/blade-poll.php?k=BLADE2026&action=status', { signal: c.signal });
/* 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 on = d.heartbeat && d.heartbeat.ts;
_fails = 0;
// Sentinel bar
document.getElementById('s-dot').className = 'dot' + (on ? ' on' : '');
document.getElementById('s-text').textContent = on ? 'Sentinel ONLINE v' + (d.heartbeat?.agent_version || '2.2') : 'Sentinel OFFLINE';
document.getElementById('s-install').style.display = on ? 'none' : 'inline-block';
// Topbar
document.getElementById('t-dot').className = 'dot' + (on ? ' on' : '');
document.getElementById('t-status').textContent = on ? 'ONLINE — Sentinel v2.2' : 'EN ATTENTE — installer Sentinel';
document.getElementById('t-status').style.color = on ? 'var(--green)' : 'var(--red)';
// Health metrics
if (d.heartbeat) {
const h = d.heartbeat;
document.getElementById('r-cpu').textContent = h.cpu || '—';
document.getElementById('r-ram').textContent = h.ram || '—';
document.getElementById('r-disk').textContent = h.disk || '—';
document.getElementById('r-up').textContent = h.uptime || '—';
document.getElementById('r-user').textContent = h.user || '—';
document.getElementById('r-ts').textContent = h.ts ? new Date(h.ts).toLocaleTimeString('fr-FR') : '—';
const cpuPct = parseInt(h.cpu) || 0, ramPct = parseInt(h.ram) || 0, diskPct = parseInt(h.disk) || 0;
document.getElementById('bar-cpu').style.width = cpuPct + '%';
document.getElementById('bar-ram').style.width = ramPct + '%';
document.getElementById('bar-disk').style.width = diskPct + '%';
document.getElementById('bar-cpu').style.background = cpuPct > 80 ? 'var(--red)' : cpuPct > 50 ? 'var(--amber)' : 'var(--blue)';
document.getElementById('bar-ram').style.background = ramPct > 85 ? 'var(--red)' : ramPct > 60 ? 'var(--amber)' : 'var(--purple)';
document.getElementById('bar-disk').style.background = diskPct > 85 ? 'var(--red)' : diskPct > 70 ? 'var(--amber)' : 'var(--cyan)';
}
// Queue
if (d.stats) document.getElementById('r-count').textContent = '(' + (d.stats.pending || 0) + ' PENDING)';
// Mini tasks
try {
const tl = await api({action:'list'});
document.getElementById('r-tasks').innerHTML = (tl.tasks||[]).slice(0,6).map(t =>
`<div class="task-mini"><span class="tm-status ${t.status}"></span><strong>${esc(t.label).substring(0,25)}</strong><br><span style="color:var(--t3)">${t.type} · ${t.status}</span></div>`
).join('');
} catch(e) {}
} catch(e) {
_fails++;
if (_fails >= 3) {
document.getElementById('s-dot').className = 'dot';
document.getElementById('s-text').textContent = 'Sentinel: erreur connexion';
document.getElementById('t-dot').className = 'dot';
document.getElementById('t-status').textContent = 'OFFLINE';
document.getElementById('t-status').style.color = 'var(--red)';
}
}
}
// === SINGLE POLLER — 60s, only when tab visible ===
checkStatus();
setInterval(() => { if (!document.hidden) checkStatus(); }, 60000);
// === PANELS ===
function showPanel(id, btn) {
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
document.getElementById('panel-' + id).classList.add('active');
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
if (btn) btn.classList.add('active');
if (id === 'tasks') loadTasks();
}
// === PUSH TASK ===
async function pushQ(type, label, cmd) {
const r = await api({action:'push', type, cmd: cmd||type, label, source:'blade-ai', priority:'7'});
addChat('user', label);
addChat('ai', `Tâche <code>${type}</code> envoyée. ID: <code>${r.task?.id||'?'}</code>`);
if (r.task) pollResult(r.task.id);
}
async function pollResult(id, attempts) {
attempts = attempts || 0;
if (attempts > 40) return;
await new Promise(r => setTimeout(r, 3000));
const d = await api({action:'list'});
const t = d.tasks?.find(t => t.id === id);
if (t && t.status === 'done') {
addChat('ai', `Résultat de <code>${t.label}</code>:<div class="task-result">${esc(t.result||'OK')}</div>`);
} else if (t && t.status === 'failed') {
addChat('ai', `Erreur <code>${t.label}</code>:<div class="task-result" style="color:var(--red)">${esc(t.error||t.result||'Error')}</div>`);
} else { pollResult(id, attempts + 1); }
}
// === CHAT ===
function addChat(who, html) {
const el = document.getElementById('chat-messages');
el.innerHTML += `<div class="chat-msg"><div class="avatar ${who==='ai'?'ai':'user'}">${who==='ai'?'AI':'Y'}</div><div class="bubble">${html}</div></div>`;
el.scrollTop = el.scrollHeight;
}
async function sendChat() {
const input = document.getElementById('chat-in');
const msg = input.value.trim(); if (!msg) return;
input.value = '';
addChat('user', esc(msg));
// Try AI brain first
let parsed = null;
try { const r = await fetch('/api/blade-brain.php?msg=' + encodeURIComponent(msg)); /* 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.ok && d.tasks?.length) parsed = d.tasks; } catch(e) {}
// Fallback to keyword
if (!parsed) { const kp = parseIntent(msg); if (kp) parsed = [kp]; }
if (parsed && parsed.length) {
for (const t of parsed) {
if (['none','greeting'].includes(t.type)) { addChat('ai','Salut ! Dis-moi ce que tu veux faire.'); continue; }
const r = await api({action:'push', type:t.type, cmd:t.cmd||t.type, label:t.label||t.type, source:'ai', priority:'8'});
addChat('ai', `<b style="color:var(--green)">[${t.type}]</b> ${t.label||t.cmd} — ID: <code>${r.task?.id||'?'}</code>`);
if (r.task) pollResult(r.task.id);
}
} else {
addChat('ai', 'Pas compris. Essaie: <code>screenshot</code>, <code>ouvre weval</code>, <code>espace disque</code>, <code>processus</code>');
}
}
function parseIntent(msg) {
const m = msg.toLowerCase();
if (m.match(/screenshot|capture|ecran|écran/)) return {type:'screenshot',cmd:'screenshot',label:'Screenshot'};
if (m.match(/^(hi|hello|salut|bonjour|hey|yo|coucou|slt)$/i)) return {type:'greeting'};
if (m.match(/sysinfo|system|système|config/)) return {type:'sysinfo',cmd:'sysinfo',label:'System Info'};
if (m.match(/process|processus|cpu|top/)) return {type:'powershell',cmd:'Get-Process|Sort CPU -Desc|Select -First 15 Name,CPU,WS|FT',label:'Top Processes'};
if (m.match(/disque|disk|espace|space|stockage/)) return {type:'powershell',cmd:'Get-PSDrive C,D,E -EA 0|Select Name,@{N="Free(GB)";E={[math]::Round($_.Free/1GB)}},@{N="Used(GB)";E={[math]::Round($_.Used/1GB)}}|FT',label:'Disk Space'};
if (m.match(/nettoie|clean|temp/)) return {type:'powershell',cmd:'Remove-Item $env:TEMP\\* -Recurse -Force -EA 0;"Temp cleaned"',label:'Cleanup'};
if (m.match(/^ouvre |^open |^lance /i)) { const url = msg.replace(/^(ouvre|open|lance)\s+/i,'').trim(); if (url.match(/^https?:\/\//)) return {type:'open_url',cmd:url,label:'Open '+url}; }
if (m.match(/^ps:|^powershell:/i)) return {type:'powershell',cmd:msg.replace(/^(ps|powershell):\s*/i,''),label:'PowerShell'};
return null;
}
// === RECIPES ===
const RECIPES = {
morning: [{type:'open_url',cmd:'https://weval-consulting.com',label:'WEVAL'},{type:'open_url',cmd:'https://weval-consulting.com/arsenal-proxy/ceo-dashboard.html',label:'Arsenal'},{type:'open_url',cmd:'https://crm.weval-consulting.com',label:'CRM'},{type:'git_pull',cmd:'git pull',label:'Git Pull'},{type:'screenshot',cmd:'screenshot',label:'Screenshot'}],
devsetup: [{type:'git_pull',cmd:'git pull',label:'Git Pull'},{type:'powershell',cmd:'code C:\\Users\\Yace\\Desktop\\CLAUDE\\weval-consulting',label:'VS Code'}],
cleanup: [{type:'powershell',cmd:'Remove-Item $env:TEMP\\* -Recurse -Force -EA 0;"Cleaned"',label:'Cleanup Temp'}],
security: [{type:'powershell',cmd:'Get-NetFirewallRule -Enabled True -Direction Inbound|Select DisplayName,Action|FT -Auto',label:'Firewall'},{type:'powershell',cmd:'Get-NetTCPConnection -State Listen|Select LocalPort,OwningProcess|Sort LocalPort|FT',label:'Ports'}],
endday: [{type:'git_push',cmd:'git push',label:'Git Push'},{type:'screenshot',cmd:'screenshot',label:'Screenshot'},{type:'notify',cmd:'Fin de journée — tout est sauvé!',label:'Notification'}],
};
async function runRecipe(name) {
const steps = RECIPES[name]; if (!steps) return;
addChat('ai', `🚀 Lancement automation <b>${name}</b> (${steps.length} étapes)...`);
for (const s of steps) await pushQ(s.type, s.label, s.cmd);
}
// === TASKS ===
async function loadTasks() {
try {
const d = await api({action:'list'});
const el = document.getElementById('tasks-full');
if (!d.tasks?.length) { el.innerHTML = '<div class="loading">Aucune tâche</div>'; return; }
el.innerHTML = d.tasks.map(t => `<div style="background:var(--s2);border:1px solid var(--brd);border-radius:8px;padding:12px;margin-bottom:8px"><div style="display:flex;justify-content:space-between"><strong>${esc(t.label)}</strong><span style="font-size:10px;padding:2px 8px;border-radius:99px;background:${t.status==='done'?'rgba(0,224,158,.12)':t.status==='failed'?'rgba(255,77,106,.12)':'rgba(255,181,71,.12)'};color:${t.status==='done'?'var(--green)':t.status==='failed'?'var(--red)':'var(--amber)'}">${t.status}</span></div>${t.result?`<pre style="font-family:var(--mono);font-size:11px;color:var(--cyan);margin-top:6px;background:var(--s1);padding:8px;border-radius:4px;max-height:100px;overflow:auto;white-space:pre-wrap">${esc(t.result).substring(0,500)}</pre>`:''}<div style="font-size:10px;color:var(--t3);margin-top:4px">${t.created?new Date(t.created).toLocaleString('fr-FR'):''} · ${t.type}</div></div>`).join('');
} catch(e) {}
}
async function clearTasks() { await api({action:'clear'}); loadTasks(); }
// === FILES ===
async function browseDir() {
const p = document.getElementById('file-path').value;
pushQ('list_dir', 'Browse ' + p, p);
}
// === TASK POLLER (Blade→Browser execution) ===
async function pollAndExecute() {
if (document.hidden) return;
try {
const r = await fetch('/api/blade-poll.php?k=BLADE2026&action=poll');
/* 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.task && d.task.command) {
await fetch('/api/blade-poll.php?k=BLADE2026&action=done&file=' + encodeURIComponent(d.task._file) + '&result=browser_dispatched');
if (d.task.name && d.task.name.includes('RELOAD')) location.reload();
}
} catch(e) {}
}
setInterval(pollAndExecute, 60000);
</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/a11y-auto-enhancer.js" defer></script>
<!-- WTP_UDOCK_V1 (Opus 21-avr t32b4) --><script src="/wtp-unified-dock.js" defer></script>
<script src="/opus-antioverlap-doctrine.js?v=1776776094" defer></script>
<!-- Opus v17 · Claude Pattern SSE (auto-injected) -->
<style id="opus-pattern-style">
#opus-pattern-badge{position:fixed;bottom:20px;right:20px;z-index:99990;
background:linear-gradient(135deg,#06b6d4,#8b5cf6);color:#fff;
padding:10px 16px;border-radius:20px;font:700 0.78rem -apple-system,sans-serif;
cursor:pointer;box-shadow:0 4px 16px rgba(0,0,0,0.35);transition:all 0.2s;
display:flex;align-items:center;gap:6px}
#opus-pattern-badge:hover{transform:translateY(-2px);box-shadow:0 6px 20px rgba(6,182,212,0.4)}
#opus-pattern-modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.8);
z-index:99991;align-items:center;justify-content:center;padding:20px}
#opus-pattern-modal.show{display:flex}
#opus-pattern-box{background:#0b0d15;color:#e2e8f0;border:1px solid rgba(6,182,212,0.3);
border-radius:14px;padding:22px;max-width:820px;width:100%;max-height:85vh;overflow:auto;
font:-apple-system,sans-serif}
#opus-pattern-box h3{font:800 1.2rem;margin-bottom:12px;
background:linear-gradient(135deg,#06b6d4,#ec4899);
-webkit-background-clip:text;-webkit-text-fill-color:transparent}
#opus-pattern-input{width:100%;background:#1a1f3a;color:#fff;border:1px solid rgba(100,116,139,0.3);
border-radius:8px;padding:10px;margin-bottom:10px;font:0.9rem -apple-system}
#opus-pattern-run{background:linear-gradient(135deg,#10b981,#06b6d4);color:#fff;border:0;
padding:10px 20px;border-radius:8px;font:700 0.85rem;cursor:pointer;margin-bottom:14px}
.phase-card{background:rgba(15,23,42,0.8);border:1px solid rgba(100,116,139,0.2);
border-left:3px solid #06b6d4;border-radius:8px;padding:10px 14px;margin-bottom:8px;
font-size:0.82rem}
.phase-card.done{border-left-color:#22c55e}
.phase-card.active{border-left-color:#f59e0b;animation:pulse 1.2s ease infinite}
.phase-name{font-weight:800;color:#06b6d4;margin-bottom:4px;font-size:0.78rem;text-transform:uppercase;letter-spacing:1px}
.phase-data{font-size:0.72rem;color:#94a3b8;font-family:ui-monospace,monospace}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.6}}
#opus-pattern-close{position:absolute;top:14px;right:20px;background:0;border:0;color:#94a3b8;
font-size:1.6rem;cursor:pointer}
/* === WEVIA Gemini Rolling v2 VISIBLE Enrichment (wave 306 batch) === */
.kpi,[class*="card"],[class*="panel"],[class*="room"],.stat-card,.metric-card,.hub-card,.widget,.stat,.box{position:relative!important}
.kpi,[class*="card"],.stat-card,.metric-card,.hub-card{animation:geV2Entrance .8s cubic-bezier(.34,1.56,.64,1) backwards}
.kpi:nth-child(1),[class*="card"]:nth-child(1){animation-delay:0s}
.kpi:nth-child(2),[class*="card"]:nth-child(2){animation-delay:.09s}
.kpi:nth-child(3),[class*="card"]:nth-child(3){animation-delay:.18s}
.kpi:nth-child(4),[class*="card"]:nth-child(4){animation-delay:.27s}
.kpi:nth-child(5),[class*="card"]:nth-child(5){animation-delay:.36s}
.kpi:nth-child(6),[class*="card"]:nth-child(6){animation-delay:.45s}
@keyframes geV2Entrance{from{opacity:0;transform:translateY(24px) scale(.94)}to{opacity:1;transform:translateY(0) scale(1)}}
.kpi,[class*="card"],.stat-card,.metric-card,.hub-card,.widget{border:1px solid transparent!important;box-shadow:0 0 0 1px rgba(236,72,153,.15),0 4px 16px rgba(0,0,0,.25)!important;transition:box-shadow .4s,transform .3s cubic-bezier(.34,1.56,.64,1),filter .3s!important}
.kpi:hover,[class*="card"]:hover,.stat-card:hover,.metric-card:hover,.hub-card:hover{transform:translateY(-6px) scale(1.03)!important;filter:brightness(1.2)!important;box-shadow:0 0 0 2px rgba(236,72,153,.6),0 12px 32px rgba(236,72,153,.25),0 0 24px rgba(78,205,196,.2)!important}
.kpi::before,[class*="card"]::before,.stat-card::before,.metric-card::before,.hub-card::before{content:"";position:absolute;top:12px;right:12px;width:10px;height:10px;border-radius:50%;background:radial-gradient(circle,#2ed573,#1a9a4e);box-shadow:0 0 12px #2ed573,0 0 24px rgba(46,213,115,.5);animation:geV2Pulse 1.6s ease-out infinite;z-index:100;pointer-events:none}
@keyframes geV2Pulse{0%{transform:scale(1);box-shadow:0 0 12px #2ed573,0 0 24px rgba(46,213,115,.5)}50%{transform:scale(1.4);box-shadow:0 0 20px #2ed573,0 0 40px rgba(46,213,115,.8)}100%{transform:scale(1);box-shadow:0 0 12px #2ed573,0 0 24px rgba(46,213,115,.5)}}
body::after{content:"";position:fixed;inset:0;pointer-events:none;background:radial-gradient(ellipse at 70% 30%,transparent 40%,rgba(236,72,153,.06) 100%),radial-gradient(ellipse at 30% 70%,transparent 40%,rgba(78,205,196,.04) 100%);animation:geV2Ambient 10s ease-in-out infinite;z-index:0}
@keyframes geV2Ambient{0%,100%{opacity:.5}50%{opacity:1}}
h1,.header-title,.main-title,.hub-title,.page-title{background-image:linear-gradient(90deg,currentColor 0%,currentColor 40%,rgba(236,72,153,1) 50%,currentColor 60%,currentColor 100%)!important;background-size:200% auto!important;-webkit-background-clip:text!important;background-clip:text!important;-webkit-text-fill-color:transparent!important;animation:geV2Shimmer 5s linear infinite!important}
@keyframes geV2Shimmer{0%{background-position:200% center}100%{background-position:-200% center}}
/* Doctrine zero chevauchement - hide common offenders */
.opus-x-btn,.toggle-top-right-btn,.fab-corner{display:none!important}
/* === end WEVIA Gemini Rolling v2 batch === */
</style>
<div id="opus-pattern-badge" onclick="window.__opusPatternOpen()">
<span>🧠</span><span>Claude Pattern</span>
</div>
<div id="opus-pattern-modal" onclick="if(event.target.id==='opus-pattern-modal')window.__opusPatternClose()">
<div id="opus-pattern-box">
<button id="opus-pattern-close" onclick="window.__opusPatternClose()">×</button>
<h3>🧠 Claude Pattern · 7 phases REAL (SSE live)</h3>
<p style="font-size:0.82rem;color:#94a3b8;margin-bottom:12px">Backend: <b id="opus-pattern-bot">blade-ai</b> · anti-hallucination · langue naturelle</p>
<input id="opus-pattern-input" placeholder="Posez une question (FR ou EN)..." value="bonjour quel est le statut" />
<button id="opus-pattern-run" onclick="window.__opusPatternRun()">▶ Lancer (SSE stream)</button>
<div id="opus-pattern-output"></div>
</div>
</div>
<script>
(function(){
const BOT = 'blade-ai';
window.__opusPatternOpen = () => document.getElementById('opus-pattern-modal').classList.add('show');
window.__opusPatternClose = () => document.getElementById('opus-pattern-modal').classList.remove('show');
window.__opusPatternRun = () => {
const msg = document.getElementById('opus-pattern-input').value.trim();
if (!msg) return;
const out = document.getElementById('opus-pattern-output');
out.innerHTML = '';
const OPUS_SESSION_KEY = 'opus_chatbot_session_' + BOT;
let sess = localStorage.getItem(OPUS_SESSION_KEY);
if (!sess) {
sess = 'opus-' + BOT + '-' + Date.now().toString(36) + '-' + Math.random().toString(36).substr(2, 6);
localStorage.setItem(OPUS_SESSION_KEY, sess);
}
// CF_BYPASS_V23 · direct 127.0.0.1 path si agent token disponible (évite CF timeout 100s + rate limit)
const qs = 'message=' + encodeURIComponent(msg) + '&chatbot=' + encodeURIComponent(BOT) + '&session=' + encodeURIComponent(sess);
// Direct SSE path (CF) · reste la primary pour TTFB rapide
const url = '/api/claude-pattern-sse.php?' + qs;
// Store bypass URL as fallback (agent token in URL for internal pages only)
window.__opusBypassUrl = '/api/cf-bypass-helper.php?target=' + encodeURIComponent('/api/claude-pattern-sse.php?' + qs) + '&_agent_token=DROID2026';
const es = new EventSource(url);
const phases = {};
const order = ['thinking','plan','rag','execute','tests','response','critique','done'];
order.forEach(p => {
const card = document.createElement('div');
card.className = 'phase-card';
card.id = 'phase-' + p;
card.innerHTML = '<div class="phase-name">' + p.toUpperCase() + '</div><div class="phase-data">⏳ waiting...</div>';
out.appendChild(card);
});
order.forEach(evName => {
es.addEventListener(evName, (e) => {
const data = JSON.parse(e.data);
const card = document.getElementById('phase-' + evName);
if (card) {
card.classList.add('done');
card.classList.remove('active');
let txt;
if (evName === 'response' && data.text) {
txt = '<div style="background:rgba(6,182,212,0.1);padding:10px;border-radius:6px;margin-top:6px;color:#e2e8f0;font-size:0.82rem">' + (data.text.substring(0, 600)) + (data.text.length > 600 ? '...' : '') + '</div>';
} else if (evName === 'tests') {
txt = '<div>' + data.passed + '/' + data.total + ' tests ✓</div>';
} else if (evName === 'critique') {
txt = '<div>Quality: <b style="color:' + (data.quality === 'EXCELLENT' ? '#22c55e' : (data.quality === 'OK' ? '#f59e0b' : '#ef4444')) + '">' + data.quality + '</b> (' + (data.quality_score * 5).toFixed(0) + '/5)</div>';
} else {
txt = JSON.stringify(data).substring(0, 300);
}
card.querySelector('.phase-data').innerHTML = txt;
}
if (evName === 'done') es.close();
});
});
es.addEventListener('error', () => es.close());
};
})();
</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>