384 lines
24 KiB
HTML
Executable File
384 lines
24 KiB
HTML
Executable File
<!DOCTYPE html><html lang="fr"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
<title>SENTINEL V3</title>
|
|
<style>
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
:root{--bg:#060a14;--s:#0c1220;--s2:#131b2e;--b:#1e2a42;--t:#e2e8f0;--d:#64748b;--cy:#22d3ee;--gn:#22c55e;--rd:#ef4444;--am:#eab308;--pu:#a855f7;--pk:#ec4899;--og:#f97316}
|
|
body{font-family:'DM Sans',sans-serif;background:var(--bg);color:var(--t);height:100vh;display:flex;flex-direction:column;overflow:hidden}
|
|
.top{background:var(--s);border-bottom:1px solid var(--b);padding:10px 20px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0}
|
|
.top h1{font-size:16px;display:flex;align-items:center;gap:8px}.top span{color:var(--cy)}
|
|
.pills{display:flex;gap:4px}
|
|
.pill{padding:5px 12px;border-radius:16px;border:1px solid var(--b);background:var(--s);font-size:10px;cursor:pointer;color:var(--d);transition:.2s}
|
|
.pill:hover,.pill.a{border-color:var(--cy);color:var(--cy);background:rgba(34,211,238,.08)}
|
|
.container{flex:1;display:flex;overflow:hidden}
|
|
.left{flex:1;overflow-y:auto;padding:14px}
|
|
.right{width:380px;border-left:1px solid var(--b);display:flex;flex-direction:column;background:var(--s);flex-shrink:0}
|
|
.g4{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:14px}
|
|
.kpi{background:var(--s);border:1px solid var(--b);border-radius:8px;padding:12px;text-align:center}
|
|
.kpi .n{font-size:24px;font-weight:700;font-family:'JetBrains Mono',monospace}
|
|
.kpi .l{font-size:8px;text-transform:uppercase;color:var(--d);margin-top:3px;letter-spacing:1px}
|
|
.card{background:var(--s);border:1px solid var(--b);border-radius:8px;padding:12px;margin-bottom:10px}
|
|
.card h3{font-size:12px;margin-bottom:8px;display:flex;align-items:center;gap:6px}
|
|
table{width:100%;border-collapse:collapse;font-size:10px}
|
|
th{text-align:left;padding:4px 6px;color:var(--d);font-size:8px;text-transform:uppercase;border-bottom:1px solid var(--b)}
|
|
td{padding:4px 6px;border-bottom:1px solid rgba(255,255,255,.02);font-family:'JetBrains Mono',monospace;font-size:10px}
|
|
.badge{padding:1px 5px;border-radius:3px;font-size:8px;font-weight:600;text-transform:uppercase}
|
|
.badge.up,.badge.active{background:rgba(34,197,94,.15);color:var(--gn)}
|
|
.badge.down,.badge.critical{background:rgba(239,68,68,.15);color:var(--rd)}
|
|
.badge.medium,.badge.high{background:rgba(249,115,22,.15);color:var(--og)}
|
|
.btn{padding:6px 14px;border:none;border-radius:6px;cursor:pointer;font-weight:600;font-size:10px;background:var(--cy);color:#000}
|
|
.btn:hover{opacity:.8}
|
|
/* Chat */
|
|
.ch{padding:10px 14px;border-bottom:1px solid var(--b);display:flex;align-items:center;gap:6px}
|
|
.ch h3{font-size:12px;color:var(--cy)}.ch .dot{width:7px;height:7px;border-radius:50%;background:var(--gn);animation:b 2s infinite}
|
|
@keyframes b{0%,100%{opacity:1}50%{opacity:.3}}
|
|
.msgs{flex:1;overflow-y:auto;padding:10px;display:flex;flex-direction:column;gap:6px}
|
|
.m{padding:8px 10px;border-radius:8px;max-width:95%;font-size:11px;line-height:1.5;white-space:pre-wrap;word-break:break-word}
|
|
.m.u{background:rgba(34,211,238,.1);border:1px solid rgba(34,211,238,.2);align-self:flex-end;color:var(--cy)}
|
|
.m.b{background:var(--s2);border:1px solid var(--b);align-self:flex-start}
|
|
.m.s{background:rgba(168,85,247,.1);border:1px solid rgba(168,85,247,.2);align-self:center;font-size:9px;color:var(--pu)}
|
|
.m .prov{font-size:8px;color:var(--d);margin-top:4px}
|
|
.ci{padding:10px;border-top:1px solid var(--b);display:flex;gap:6px}
|
|
.ci input{flex:1;background:var(--s2);border:1px solid var(--b);border-radius:6px;padding:8px 12px;color:var(--t);font-size:11px;font-family:inherit}
|
|
.ci input:focus{outline:none;border-color:var(--cy)}
|
|
.ci button{background:var(--cy);color:#000;border:none;border-radius:6px;padding:8px 14px;font-weight:700;font-size:11px;cursor:pointer}
|
|
.qb{padding:6px 10px;display:flex;gap:3px;flex-wrap:wrap;border-top:1px solid var(--b)}
|
|
.qb button{padding:3px 8px;border-radius:10px;border:1px solid var(--b);background:transparent;color:var(--d);font-size:8px;cursor:pointer}
|
|
.qb button:hover{border-color:var(--cy);color:var(--cy)}
|
|
/* Architecture diagram */
|
|
.arch-svg{width:100%;background:var(--s);border:1px solid var(--b);border-radius:8px;overflow:hidden}
|
|
.arch-svg text{font-family:'JetBrains Mono',monospace}
|
|
.node{cursor:pointer;transition:opacity .2s}.node:hover{opacity:.8}
|
|
.tl{display:flex;gap:6px;padding:4px 6px;border-radius:4px;background:var(--s2);font-size:9px;align-items:flex-start}
|
|
.tl .time{color:var(--d);white-space:nowrap;min-width:45px;font-size:8px}
|
|
.tl .tag{color:var(--cy);font-weight:600;font-size:8px}
|
|
.timeline{display:flex;flex-direction:column;gap:4px;max-height:160px;overflow-y:auto}
|
|
.loading{display:inline-block;width:12px;height:12px;border:2px solid var(--b);border-top:2px solid var(--cy);border-radius:50%;animation:spin .8s linear infinite}
|
|
@keyframes spin{to{transform:rotate(360deg)}}
|
|
</style>
|
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
</head><body>
|
|
<div class="top">
|
|
<div><h1>🛡️ <span>SENTINEL</span> V3</h1></div>
|
|
<div style="display:flex;gap:6px;align-items:center">
|
|
<div class="pills">
|
|
<div class="pill a" onclick="showTab('dash',this)">Dashboard</div>
|
|
<div class="pill" onclick="showTab('arch',this)">🏗️ Architecture</div>
|
|
<div class="pill" onclick="showTab('patterns',this)">🧠 Patterns</div>
|
|
<div class="pill" onclick="showTab('hist',this)">📜 Historique</div>
|
|
</div>
|
|
<button class="btn" onclick="runScan(false)">🔍 Scan</button>
|
|
<button class="btn" style="background:var(--gn)" onclick="runScan(true)">🔧 Fix</button>
|
|
<span id="lastScan" style="font-size:9px;color:var(--d)">—</span>
|
|
</div>
|
|
</div>
|
|
<div class="container">
|
|
<div class="left">
|
|
<div class="g4">
|
|
<div class="kpi"><div class="n" style="color:var(--cy)" id="k-score">—</div><div class="l">Score</div></div>
|
|
<div class="kpi"><div class="n" style="color:var(--gn)" id="k-files">—</div><div class="l">Fichiers</div></div>
|
|
<div class="kpi"><div class="n" style="color:var(--am)" id="k-issues">—</div><div class="l">Issues</div></div>
|
|
<div class="kpi"><div class="n" style="color:var(--gn)" id="k-fixed">—</div><div class="l">Réparées</div></div>
|
|
</div>
|
|
|
|
<!-- TAB: Dashboard -->
|
|
<div id="tabDash">
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
|
<div class="card"><h3>⚠️ Issues</h3><div id="issuesList" style="max-height:200px;overflow-y:auto"><div style="color:var(--d);font-size:10px">Lancez un scan</div></div></div>
|
|
<div class="card"><h3>🔧 Fixes Récents</h3><div class="timeline" id="fixesTL"></div></div>
|
|
</div>
|
|
<div class="card"><h3>📊 Scans</h3><table><thead><tr><th>Date</th><th>Fichiers</th><th>Issues</th><th>Fix</th><th>Score</th><th>ms</th></tr></thead><tbody id="histBody"></tbody></table></div>
|
|
</div>
|
|
|
|
<!-- TAB: Architecture (Dynamic) -->
|
|
<div id="tabArch" style="display:none">
|
|
<div class="card"><h3>🏗️ Architecture Dynamique <span id="archTime" style="font-size:8px;color:var(--d);margin-left:auto"></span></h3>
|
|
<div id="archDiagram"></div>
|
|
</div>
|
|
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px">
|
|
<div class="card"><h3>🖥️ Serveurs</h3><div id="archServers"></div></div>
|
|
<div class="card"><h3>🌐 Ports</h3><div id="archPorts"></div></div>
|
|
<div class="card"><h3>🗄️ Base de Données</h3><div id="archDB"></div></div>
|
|
</div>
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
|
<div class="card"><h3>⚡ Système</h3><div id="archSystem"></div></div>
|
|
<div class="card"><h3>🛡️ Sentinel Activity</h3><div id="archSentinel"></div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- TAB: Patterns -->
|
|
<div id="tabPatterns" style="display:none">
|
|
<div class="card"><h3>🧠 Patterns Appris</h3><table><thead><tr><th>Pattern</th><th>Détections</th><th>Sévérité</th><th>Dernière</th></tr></thead><tbody id="patBody"></tbody></table></div>
|
|
</div>
|
|
|
|
<!-- TAB: History -->
|
|
<div id="tabHist" style="display:none">
|
|
<div class="card"><h3>📜 Historique Complet</h3><table><thead><tr><th>Date</th><th>Type</th><th>Fichiers</th><th>Issues</th><th>Fix</th><th>Score</th></tr></thead><tbody id="fullHistBody"></tbody></table></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- RIGHT: Chat -->
|
|
<div class="right">
|
|
<div class="ch"><div class="dot"></div><h3>Sentinel IA</h3><span style="font-size:8px;color:var(--d);margin-left:auto" id="chatProv">LLM</span></div>
|
|
<div class="msgs" id="msgs">
|
|
<div class="m s">🛡️ Sentinel V3 connecté à HAMID (7 providers IA). Je scanne, diagnostique et RÉPARE. Parlez-moi en langage naturel.</div>
|
|
</div>
|
|
<div class="qb">
|
|
<button onclick="sq('scan le serveur')">🔍 Scan</button>
|
|
<button onclick="sq('répare tout')">🔧 Fix</button>
|
|
<button onclick="sq('état du système')">📊 Status</button>
|
|
<button onclick="sq('vérifie les ports')">🌐 Ports</button>
|
|
<button onclick="sq('audite le menu sidebar')">📋 Menu</button>
|
|
<button onclick="sq('exec uptime')">💻 SSH</button>
|
|
<button onclick="sq('vérifie le tracking OVH')">📡 Tracking</button>
|
|
</div>
|
|
<div class="ci">
|
|
<input type="text" id="chatIn" placeholder="Parlez en langage naturel..." autocomplete="off">
|
|
<button id="chatBtn" onclick="sendChat()">→</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const API='/api/sentinel-brain.php';
|
|
let archData=null;
|
|
|
|
async function api(action,p={}){
|
|
try{
|
|
if(p._post){const fd=new FormData();Object.entries(p).forEach(([k,v])=>{if(k!=='_post')fd.append(k,v)});const r=await fetch(`${API}?action=${action}`,{method:'POST',body:fd});return await r.json();}
|
|
const qs=Object.entries(p).map(([k,v])=>`${k}=${encodeURIComponent(v)}`).join('&');
|
|
return await(await fetch(`${API}?action=${action}${qs?'&'+qs:''}`)).json();
|
|
}catch(e){return{error:e.message}}
|
|
}
|
|
|
|
// === CHAT ===
|
|
async function sendChat(){
|
|
const inp=document.getElementById('chatIn'),msg=inp.value.trim();
|
|
if(!msg)return; inp.value=''; addMsg(msg,'u');
|
|
const btn=document.getElementById('chatBtn');btn.disabled=true;btn.innerHTML='<div class="loading"></div>';
|
|
try{
|
|
const d=await api('chat',{message:msg,_post:true});
|
|
addMsg(d.response||d.error||'Erreur','b',d.provider);
|
|
if(d.executed?.length){
|
|
d.executed.forEach(e=>{const t=e.cmd.startsWith("[OVH]")?"📡":e.cmd.startsWith("[HETZNER]")?"🖥️":"💻";addMsg(t+" "+e.cmd+"\n"+(e.ok?"✅":"❌")+" "+e.output,"s")});
|
|
}
|
|
if(d.provider)document.getElementById('chatProv').textContent=d.provider;
|
|
}catch(e){addMsg('❌ '+e.message,'b')}
|
|
btn.disabled=false;btn.textContent='→';
|
|
}
|
|
function sq(c){document.getElementById('chatIn').value=c;sendChat()}
|
|
function addMsg(t,type,prov=''){
|
|
const c=document.getElementById('msgs'),d=document.createElement('div');
|
|
d.className=`m ${type}`;d.textContent=t;
|
|
if(prov&&type==='b'){const p=document.createElement('div');p.className='prov';p.textContent='via '+prov;d.appendChild(p);}
|
|
c.appendChild(d);c.scrollTop=c.scrollHeight;
|
|
}
|
|
document.getElementById('chatIn').addEventListener('keydown',e=>{if(e.key==='Enter')sendChat()});
|
|
|
|
// === TABS ===
|
|
function showTab(t,el){
|
|
['Dash','Arch','Patterns','Hist'].forEach(x=>document.getElementById('tab'+x).style.display='none');
|
|
document.getElementById('tab'+t.charAt(0).toUpperCase()+t.slice(1)).style.display='block';
|
|
document.querySelectorAll('.pill').forEach(p=>p.classList.remove('a'));if(el)el.classList.add('a');
|
|
if(t==='arch')loadArch();if(t==='patterns')loadPatterns();if(t==='hist')loadFullHist();
|
|
}
|
|
|
|
// === ARCHITECTURE DIAGRAM ===
|
|
async function loadArch(){
|
|
const d=await api('arch');if(d.error)return;archData=d;
|
|
document.getElementById('archTime').textContent='Mise à jour: '+new Date(d.last_updated).toLocaleTimeString('fr-FR');
|
|
renderArchDiagram(d);renderArchDetails(d);
|
|
}
|
|
|
|
function renderArchDiagram(d){
|
|
const W=900,H=500;
|
|
let svg=`<svg viewBox="0 0 ${W} ${H}" class="arch-svg" style="height:${H}px">`;
|
|
svg+=`<defs><filter id="glow"><feGaussianBlur stdDeviation="3" result="g"/><feMerge><feMergeNode in="g"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs>`;
|
|
svg+=`<rect width="${W}" height="${H}" fill="#060a14"/>`;
|
|
|
|
// Server boxes
|
|
const servers=[
|
|
{x:50,y:30,w:550,h:420,name:'HETZNER 89.167.40.150',sub:'Ubuntu 24 • 16GB RAM',color:'#22d3ee',up:d.servers?.hetzner?.reachable},
|
|
{x:650,y:30,w:220,h:160,name:'OVH TRACKING',sub:'151.80.235.110 • ubuntu',color:'#a855f7',up:true},
|
|
{x:650,y:220,w:220,h:100,name:'WEVAL CONSULTING',sub:'46.62.220.135',color:'#f97316',up:false}
|
|
];
|
|
servers.forEach(s=>{
|
|
const op=s.up?1:.4;
|
|
svg+=`<rect x="${s.x}" y="${s.y}" width="${s.w}" height="${s.h}" rx="8" fill="none" stroke="${s.color}" stroke-width="1.5" opacity="${op}"/>`;
|
|
svg+=`<text x="${s.x+10}" y="${s.y+18}" fill="${s.color}" font-size="11" font-weight="700" opacity="${op}">${s.name}</text>`;
|
|
svg+=`<text x="${s.x+10}" y="${s.y+32}" fill="#64748b" font-size="9" opacity="${op}">${s.sub}</text>`;
|
|
});
|
|
|
|
// Port boxes inside Hetzner
|
|
const ports=[
|
|
{x:70,y:60,w:160,h:70,name:'ADX :5821',sub:'Principal',k:'adx',color:'#22d3ee'},
|
|
{x:250,y:60,w:160,h:70,name:'ARSENAL :5890',sub:'APIs + Screens',k:'arsenal',color:'#22c55e'},
|
|
{x:430,y:60,w:150,h:35,name:'FMGAPP :5822',k:'fmg',color:'#64748b'},
|
|
{x:430,y:100,w:150,h:35,name:'BCGAPP :5823',k:'bcg',color:'#64748b'},
|
|
{x:70,y:150,w:120,h:50,name:'PostgreSQL',sub:':5432',k:'postgres',color:'#f97316'},
|
|
{x:210,y:150,w:120,h:50,name:'Ollama LLM',sub:':11434',k:'ollama',color:'#ec4899'},
|
|
{x:350,y:150,w:120,h:50,name:'N8N',sub:':8080',k:'n8n',color:'#a855f7'},
|
|
{x:490,y:150,w:100,h:50,name:'DKIM',sub:':5824',k:'dkim',color:'#64748b'}
|
|
];
|
|
ports.forEach(p=>{
|
|
const st=d.ports?.[p.k]?.status||'unknown';
|
|
const col=st==='up'?p.color:'#ef4444';const op=st==='up'?1:.5;
|
|
svg+=`<rect x="${p.x}" y="${p.y}" width="${p.w}" height="${p.h}" rx="5" fill="rgba(${hexToRgb(col)},.08)" stroke="${col}" stroke-width="1" opacity="${op}"/>`;
|
|
svg+=`<circle cx="${p.x+p.w-10}" cy="${p.y+10}" r="4" fill="${st==='up'?'#22c55e':'#ef4444'}"/>`;
|
|
svg+=`<text x="${p.x+8}" y="${p.y+16}" fill="${col}" font-size="10" font-weight="600">${p.name}</text>`;
|
|
if(p.sub)svg+=`<text x="${p.x+8}" y="${p.y+30}" fill="#64748b" font-size="8">${p.sub}</text>`;
|
|
});
|
|
|
|
// Connections
|
|
svg+=`<line x1="230" y1="95" x2="250" y2="95" stroke="#22d3ee" stroke-width="1.5" stroke-dasharray="4"/>`;
|
|
svg+=`<text x="232" y="88" fill="#22d3ee" font-size="7">ProxyPass /api/</text>`;
|
|
svg+=`<line x1="330" y1="130" x2="130" y2="150" stroke="#f97316" stroke-width="1" stroke-dasharray="3" opacity=".5"/>`;
|
|
svg+=`<line x1="330" y1="130" x2="270" y2="150" stroke="#ec4899" stroke-width="1" stroke-dasharray="3" opacity=".5"/>`;
|
|
|
|
// Connection to OVH
|
|
svg+=`<line x1="410" y1="95" x2="650" y2="95" stroke="#a855f7" stroke-width="1" stroke-dasharray="5"/>`;
|
|
svg+=`<text x="510" y="88" fill="#a855f7" font-size="7">Tracking pixels</text>`;
|
|
|
|
// Brain Engine box
|
|
svg+=`<rect x="70" y="220" width="250" height="90" rx="5" fill="rgba(34,211,238,.05)" stroke="#22d3ee" stroke-width="1"/>`;
|
|
svg+=`<text x="80" y="240" fill="#22d3ee" font-size="10" font-weight="700">🧠 BRAIN ENGINE</text>`;
|
|
svg+=`<text x="80" y="255" fill="#94a3b8" font-size="8">${d.databases?.brain_winners||0} winning configs • 88-100% inbox</text>`;
|
|
svg+=`<text x="80" y="268" fill="#94a3b8" font-size="8">ISP optimization • Auto-combo discovery</text>`;
|
|
|
|
// HAMID box
|
|
svg+=`<rect x="340" y="220" width="250" height="90" rx="5" fill="rgba(168,85,247,.05)" stroke="#a855f7" stroke-width="1"/>`;
|
|
svg+=`<text x="350" y="240" fill="#a855f7" font-size="10" font-weight="700">🤖 HAMID IA</text>`;
|
|
svg+=`<text x="350" y="255" fill="#94a3b8" font-size="8">7 providers • ${d.databases?.hamid_conversations||0} conversations</text>`;
|
|
svg+=`<text x="350" y="268" fill="#94a3b8" font-size="8">Cerebras → Groq → DeepSeek → Gemini → Claude</text>`;
|
|
|
|
// Office 365 box
|
|
svg+=`<rect x="70" y="330" width="250" height="50" rx="5" fill="rgba(249,115,22,.05)" stroke="#f97316" stroke-width="1"/>`;
|
|
svg+=`<text x="80" y="350" fill="#f97316" font-size="10" font-weight="700">📧 Office 365</text>`;
|
|
svg+=`<text x="80" y="365" fill="#94a3b8" font-size="8">${d.databases?.office_accounts||0} comptes • 176 tenants</text>`;
|
|
|
|
// SENTINEL box (bottom)
|
|
svg+=`<rect x="340" y="330" width="250" height="100" rx="5" fill="rgba(34,197,94,.05)" stroke="#22c55e" stroke-width="2" filter="url(#glow)"/>`;
|
|
svg+=`<text x="350" y="352" fill="#22c55e" font-size="11" font-weight="700">🛡️ SENTINEL V3</text>`;
|
|
svg+=`<text x="350" y="367" fill="#94a3b8" font-size="8">${d.sentinel?.total_scans||0} scans • ${d.sentinel?.total_fixes||0} fixes • ${d.sentinel?.patterns_known||0} patterns</text>`;
|
|
const ls=d.sentinel?.last_scan;
|
|
if(ls)svg+=`<text x="350" y="382" fill="${(ls.score||0)>=95?'#22c55e':'#eab308'}" font-size="9" font-weight="700">Score: ${ls.score}%</text>`;
|
|
svg+=`<text x="350" y="397" fill="#94a3b8" font-size="8">Cron 22h • Auto-repair • Learning</text>`;
|
|
svg+=`<text x="350" y="412" fill="#64748b" font-size="7">SSH Full Root • 7 LLM Providers • 24 patterns</text>`;
|
|
|
|
// OVH detail
|
|
svg+=`<text x="660" y="70" fill="#a855f7" font-size="9">Open/Click/Unsub Tracking</text>`;
|
|
svg+=`<text x="660" y="85" fill="#94a3b8" font-size="8">Ports 58421-58423</text>`;
|
|
svg+=`<text x="660" y="100" fill="#eab308" font-size="8">⚠️ user: ubuntu (NOT root)</text>`;
|
|
|
|
// System stats
|
|
svg+=`<rect x="660" y="350" width="220" height="90" rx="5" fill="rgba(255,255,255,.02)" stroke="#1e2a42"/>`;
|
|
svg+=`<text x="670" y="370" fill="#64748b" font-size="9" font-weight="600">SYSTEM</text>`;
|
|
svg+=`<text x="670" y="385" fill="#94a3b8" font-size="8">💾 Disk: ${d.system?.disk||'?'}</text>`;
|
|
svg+=`<text x="670" y="398" fill="#94a3b8" font-size="8">🧠 RAM: ${d.system?.memory||'?'}</text>`;
|
|
svg+=`<text x="670" y="411" fill="#94a3b8" font-size="8">⚡ Load: ${d.system?.load||'?'}</text>`;
|
|
svg+=`<text x="670" y="424" fill="#94a3b8" font-size="8">⏱ ${d.system?.uptime||'?'}</text>`;
|
|
|
|
svg+=`</svg>`;
|
|
document.getElementById('archDiagram').innerHTML=svg;
|
|
}
|
|
|
|
function hexToRgb(h){h=h.replace('#','');return[parseInt(h.substr(0,2),16),parseInt(h.substr(2,2),16),parseInt(h.substr(4,2),16)].join(',');}
|
|
|
|
function renderArchDetails(d){
|
|
// Servers
|
|
let sh='';
|
|
for(let[k,s] of Object.entries(d.servers||{})){
|
|
sh+=`<div style="padding:4px 0;font-size:10px;border-bottom:1px solid var(--b)">
|
|
<span style="color:${s.reachable?'var(--gn)':'var(--d)'}">●</span> <b>${s.name}</b> (${s.ip})
|
|
<div style="color:var(--d);font-size:8px">${s.os||''} • ${s.user}@${s.ip}:${s.ssh_port||22}</div></div>`;
|
|
}
|
|
document.getElementById('archServers').innerHTML=sh||'—';
|
|
|
|
// Ports
|
|
let ph='<table><thead><tr><th>Port</th><th>Service</th><th>Status</th></tr></thead><tbody>';
|
|
for(let[k,p] of Object.entries(d.ports||{})){
|
|
ph+=`<tr><td>:${p.port}</td><td>${p.name}</td><td><span class="badge ${p.status}">${p.status}</span></td></tr>`;
|
|
}
|
|
ph+='</tbody></table>';
|
|
document.getElementById('archPorts').innerHTML=ph;
|
|
|
|
// DB
|
|
let db=`<div style="font-size:10px">`;
|
|
for(let[k,v] of Object.entries(d.databases||{})){
|
|
db+=`<div style="padding:2px 0"><span style="color:var(--cy)">${k}</span>: <b>${v}</b></div>`;
|
|
}
|
|
db+=`</div>`;
|
|
document.getElementById('archDB').innerHTML=db;
|
|
|
|
// System
|
|
let sys=`<div style="font-size:10px">`;
|
|
for(let[k,v] of Object.entries(d.system||{})){
|
|
sys+=`<div style="padding:2px 0">${k}: <b style="color:var(--cy)">${v}</b></div>`;
|
|
}
|
|
for(let[k,v] of Object.entries(d.services||{})){
|
|
sys+=`<div style="padding:2px 0">${k}: <span class="badge ${v==='active'?'up':'down'}">${v}</span></div>`;
|
|
}
|
|
sys+=`</div>`;
|
|
document.getElementById('archSystem').innerHTML=sys;
|
|
|
|
// Sentinel
|
|
let se=d.sentinel||{};
|
|
let sh2=`<div style="font-size:10px">
|
|
<div>Scans: <b>${se.total_scans||0}</b> | Fixes: <b>${se.total_fixes||0}</b> | Patterns: <b>${se.patterns_known||0}</b></div>`;
|
|
if(se.recent_fixes?.length){
|
|
sh2+=`<div style="margin-top:6px;font-size:9px;color:var(--d)">Dernières réparations:</div>`;
|
|
se.recent_fixes.forEach(f=>{sh2+=`<div style="font-size:9px"><span style="color:var(--cy)">${f.issue_type}</span> → ${f.fix_applied}</div>`});
|
|
}
|
|
sh2+=`</div>`;
|
|
document.getElementById('archSentinel').innerHTML=sh2;
|
|
}
|
|
|
|
// === DATA ===
|
|
async function loadStatus(){
|
|
const d=await api('status');if(d.error)return;
|
|
if(d.last_scan){
|
|
document.getElementById('k-score').textContent=d.last_scan.score+'%';
|
|
document.getElementById('k-files').textContent=d.last_scan.total_files;
|
|
document.getElementById('k-issues').textContent=d.last_scan.issues_found;
|
|
document.getElementById('k-fixed').textContent=d.last_scan.issues_fixed;
|
|
document.getElementById('lastScan').textContent=new Date(d.last_scan.scan_date).toLocaleString('fr-FR');
|
|
}
|
|
}
|
|
async function loadHist(){
|
|
const d=await api('history',{limit:10});const tb=document.getElementById('histBody');
|
|
if(!d.scans?.length)return;
|
|
tb.innerHTML=d.scans.map(s=>`<tr><td>${new Date(s.scan_date).toLocaleString('fr-FR')}</td><td>${s.total_files}</td><td style="color:${s.issues_found>0?'var(--am)':'var(--gn)'}">${s.issues_found}</td><td style="color:var(--gn)">${s.issues_fixed}</td><td style="color:${s.score>=95?'var(--gn)':'var(--am)'};font-weight:700">${s.score}%</td><td style="color:var(--d)">${s.duration_ms}ms</td></tr>`).join('');
|
|
}
|
|
async function loadFixes(){
|
|
const d=await api('fixes',{limit:10});const tl=document.getElementById('fixesTL');
|
|
if(!d.fixes?.length){tl.innerHTML='<div style="color:var(--d);font-size:9px">—</div>';return;}
|
|
tl.innerHTML=d.fixes.map(f=>`<div class="tl"><div class="time">${new Date(f.fix_date).toLocaleTimeString('fr-FR',{hour:'2-digit',minute:'2-digit'})}</div><div><span class="tag">${f.issue_type}</span> ${(f.file_path||'').split('/').pop()}</div></div>`).join('');
|
|
}
|
|
async function loadPatterns(){
|
|
const d=await api('patterns');const tb=document.getElementById('patBody');
|
|
if(!d.patterns?.length)return;
|
|
tb.innerHTML=d.patterns.map(p=>`<tr><td style="font-weight:600">${p.pattern_name}</td><td>${p.times_detected}</td><td><span class="badge ${p.severity}">${p.severity}</span></td><td style="color:var(--d);font-size:8px">${p.last_seen?new Date(p.last_seen).toLocaleString('fr-FR'):'—'}</td></tr>`).join('');
|
|
}
|
|
async function loadFullHist(){
|
|
const d=await api('history',{limit:50});const tb=document.getElementById('fullHistBody');
|
|
if(!d.scans?.length)return;
|
|
tb.innerHTML=d.scans.map(s=>`<tr><td>${new Date(s.scan_date).toLocaleString('fr-FR')}</td><td>${s.scan_type}</td><td>${s.total_files}</td><td>${s.issues_found}</td><td>${s.issues_fixed}</td><td style="font-weight:700;color:${s.score>=95?'var(--gn)':'var(--am)'}">${s.score}%</td></tr>`).join('');
|
|
}
|
|
async function runScan(fix=false){
|
|
addMsg(fix?'🔧 Scan + repair...':'🔍 Scanning...','s');
|
|
const d=await api('scan',{fix:fix?'1':'0'});
|
|
if(d.score!==undefined){
|
|
document.getElementById('k-score').textContent=d.score+'%';document.getElementById('k-files').textContent=d.total_files;
|
|
document.getElementById('k-issues').textContent=d.issues_found;document.getElementById('k-fixed').textContent=d.issues_fixed;
|
|
const il=document.getElementById('issuesList');
|
|
if(d.issues?.length)il.innerHTML='<table><tbody>'+d.issues.map(i=>`<tr><td>${i.file}</td><td><span class="badge ${i.type.includes('NUKE')?'critical':'medium'}">${i.type}</span></td></tr>`).join('')+'</tbody></table>';
|
|
else il.innerHTML='<div style="color:var(--gn);font-size:10px">✅ Sain</div>';
|
|
addMsg(`✅ ${d.score}% | ${d.total_files} fichiers | ${d.issues_found} issues | ${d.issues_fixed} fixées`,'s');
|
|
}
|
|
loadHist();loadFixes();
|
|
}
|
|
|
|
loadStatus();loadHist();loadFixes();
|
|
setInterval(loadStatus,60000);
|
|
</script>
|
|
</body></html>
|