Files
weval-consulting/wevia-director-dashboard.html
2026-04-08 01:50:03 +02:00

422 lines
18 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');
const d = await r.json();
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');
const d = await r.json();
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');
const data = await r.json();
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');
const d = await r.json();
document.getElementById('stUptime').textContent = d.uptime || '?';
} catch(e) {}
// Agents status
try {
const r2 = await fetch(MASTER_API + '?health');
const h = await r2.json();
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);
const d = await r.json();
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></body>
</html>