context-hub/templates/dashboard.html

382 lines
22 KiB
HTML

@{% extends "base.html" %}
{% block content %}
<div style="display:flex;height:100vh;width:100%;overflow:hidden;background:#0d0d0d;">
<!-- SIDEBAR -->
<aside style="width:248px;flex-shrink:0;border-right:1px solid #1b1b1b;background:#0f0f0f;display:flex;flex-direction:column;">
<div style="padding:22px 20px 18px 20px;border-bottom:1px solid #1a1a1a;">
<div style="display:flex;align-items:center;gap:9px;">
<div style="width:9px;height:9px;border-radius:2px;background:#d97757;"></div>
<div style="font-size:15px;font-weight:700;letter-spacing:0.04em;">CONCEPT</div>
</div>
<div style="margin-top:8px;font-family:'JetBrains Mono',monospace;font-size:11px;color:#5e5c58;letter-spacing:0.01em;">context.bolbol.tn:3093</div>
</div>
<nav id="nav-container" style="padding:14px 12px 8px 12px;display:flex;flex-direction:column;gap:2px;"></nav>
<div style="padding:18px 22px 8px 22px;font-size:10.5px;font-weight:600;letter-spacing:0.13em;color:#4f4d49;">SCOPES</div>
<div id="sidebar-scopes" style="padding:0 12px;display:flex;flex-direction:column;gap:1px;overflow-y:auto;flex:1;"></div>
<div style="padding:14px 20px;border-top:1px solid #1a1a1a;display:flex;align-items:center;gap:9px;">
<div style="width:7px;height:7px;border-radius:50%;background:#6fa888;animation:pulse 2.4s ease-in-out infinite;"></div>
<span style="font-size:11.5px;color:#8f8d88;">Serveur en ligne</span>
<span style="margin-left:auto;font-family:'JetBrains Mono',monospace;font-size:10.5px;color:#4f4d49;">v1.5</span>
</div>
<a href="/logout" style="margin: 0 20px 14px 20px; padding:10px 0; background:#181818; border:1px solid #222; border-radius:6px; color:#a3a19c; font-size:11.5px; font-weight:500; text-align:center; text-decoration:none; transition:all 0.2s;" onmouseover="this.style.background='#222';this.style.color='#fff';" onmouseout="this.style.background='#181818';this.style.color='#a3a19c';">
Déconnexion
</a>
<footer class="nyora-footer">
<div class="nyora-line-top"></div>
<div class="nyora-brand">
<span class="nyora-dot"></span>
<span class="nyora-name">NYORA</span>
<span class="nyora-dot"></span>
</div>
<div class="nyora-tag">Crafted with precision</div>
</footer>
</aside>
<!-- MAIN -->
<main id="main-content" style="flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0;">
</main>
<div id="toast-container"></div>
</div>
<script>
// INITIAL DATA
const scopesDef = [
{ id:'infra', label:'infra', color:'#7b95c4', desc:'Serveurs, ports et déploiement de toute la stack.', updated:'Live', agents:['Hermes','Claude'] },
{ id:'llm', label:'llm', color:'#9b87c4', desc:'Modèles, prompts système et budgets de tokens.', updated:'Live', agents:['Claude','Gemini'] },
{ id:'nyora', label:'nyora', color:'#7bb592', desc:'État, priorités et stack du projet Nyora.', updated:'Live', agents:['Hermes','Claude'] },
{ id:'perso', label:'perso', color:'#c4a878', desc:'Préférences, fuseau et style de réponse de Nabil.', updated:'Live', agents:['Claude','Gemini'] },
{ id:'tt', label:'tt', color:'#c48a7b', desc:'Repo, hôte et état du projet TT.', updated:'Live', agents:['Hermes','Claude'] },
];
let state = {
scopeData: {},
screen: 'home',
scope: 'infra',
filter: 'all',
auditData: [],
portsData: {},
llmData: {},
infraData: {},
keysData: [],
drNexum: { subs: localStorage.getItem('dr_nexum_subs')||0, rev: localStorage.getItem('dr_nexum_rev')||0 }
};
function flash(msg) {
const t = document.getElementById('toast-container');
t.innerHTML = `<div style="position:fixed;bottom:24px;right:28px;display:flex;align-items:center;gap:10px;background:#181818;border:1px solid #2c2c2c;color:#e8e6e3;padding:11px 16px;border-radius:9px;font-size:13px;box-shadow:0 8px 30px rgba(0,0,0,0.5);animation:toastIn 0.18s ease-out;z-index:50;">
<div style="width:7px;height:7px;border-radius:50%;background:#6fa888;"></div>
<span>${msg}</span>
</div>`;
setTimeout(() => t.innerHTML='', 2000);
}
function copy(txt) {
navigator.clipboard.writeText(txt).catch(()=>{});
flash('Copié !');
}
function agentColor(a){ if(a==='Claude')return '#d99a86'; if(a==='Gemini')return '#9b87c4'; return '#8aa6c4'; }
function colorOf(id){ const s=scopesDef.find(x=>x.id===id); return s?s.color:'#6a6863'; }
function go(sc) { state.screen = sc; render(); }
function openScope(id) { state.screen = 'scope'; state.scope = id; render(); }
function setFilter(f) { state.filter = f; render(); }
function updateNexum(k, v) { state.drNexum[k] = v; localStorage.setItem('dr_nexum_'+k, v); render(); }
// DATA PARSING
function getEntries(scopeId) {
let arr = [];
const data = state.scopeData[scopeId] || {};
for(let k in data) {
const v = data[k];
arr.push({ key: k, value: typeof v === 'object' ? JSON.stringify(v, null, 2) : String(v) });
}
return arr;
}
function renderSidebar() {
const scopesActive = state.screen === 'home' || state.screen === 'scope';
const navItems = [
{ id:'home', label:'Scopes', screen:'home', active: scopesActive },
{ id:'ports', label:'Ports', screen:'ports', active: state.screen==='ports' },
{ id:'keys', label:'Clés API', screen:'keys', active: state.screen==='keys' },
{ id:'activity', label:'Activité', screen:'activity', active: state.screen==='activity' },
];
let navHtml = '';
navItems.forEach(n => {
const bg = n.active ? 'background:#181818;color:#e8e6e3;font-weight:500;' : 'color:#8f8d88;';
const bar = n.active ? '#d97757' : 'transparent';
navHtml += `<div class="hover-bg-17" onclick="go('${n.screen}')" style="display:flex;align-items:center;gap:11px;padding:9px 12px 9px 14px;border-radius:7px;cursor:pointer;font-size:13.5px;position:relative;transition:background .12s;${bg}">
<div style="position:absolute;left:3px;top:9px;bottom:9px;width:2.5px;border-radius:2px;background:${bar};"></div>
<span>${n.label}</span>
</div>`;
});
document.getElementById('nav-container').innerHTML = navHtml;
let scopesHtml = '';
scopesDef.forEach(s => {
const active = (state.screen === 'scope' && state.scope === s.id);
const bg = active ? 'background:#181818;' : '';
const count = getEntries(s.id).length;
scopesHtml += `<div class="hover-bg-17" onclick="openScope('${s.id}')" style="display:flex;align-items:center;gap:10px;padding:7px 10px;border-radius:6px;cursor:pointer;${bg}">
<div style="width:8px;height:8px;border-radius:2px;flex-shrink:0;background:${s.color};"></div>
<span style="font-family:'JetBrains Mono',monospace;font-size:12.5px;flex:1;">${s.label}</span>
<span style="font-size:11px;color:#5e5c58;font-variant-numeric:tabular-nums;">${count}</span>
</div>`;
});
document.getElementById('sidebar-scopes').innerHTML = scopesHtml;
}
function renderNexum() {
const s = parseInt(state.drNexum.subs) || 0;
const r = parseInt(state.drNexum.rev) || 0;
const pS = Math.min(100, (s/1000)*100);
const pR = Math.min(100, (r/300)*100);
const cS = pS>80?'#6fa888':pS>50?'#c4a878':'#c47a6e';
const cR = pR>80?'#6fa888':pR>50?'#c4a878':'#c47a6e';
return `<div style="border:1px solid #5a352f;border-left:3px solid #c47a6e;border-radius:8px;background:linear-gradient(90deg,#1c0e0b,transparent);padding:20px 24px;margin-bottom:26px;">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px;">
<span style="font-family:'JetBrains Mono',monospace;font-size:15px;font-weight:600;color:#c47a6e;letter-spacing:0.05em;">KILL-SWITCH DR. NEXUM</span>
<span style="font-size:12px;color:#e08864;background:#2a1215;padding:3px 8px;border-radius:4px;border:1px solid #5a352f;">J-X avant 15 nov 2026</span>
</div>
<div style="display:flex;gap:40px;">
<div style="flex:1;">
<div style="display:flex;justify-content:space-between;font-size:12px;color:#8f8d88;">
<span>Abonnés YouTube</span>
<span><input type="number" value="${s}" onchange="updateNexum('subs', this.value)" style="width:50px;background:transparent;border:none;color:#e8e6e3;text-align:right;font-family:'JetBrains Mono';font-size:12px;"> / 1000</span>
</div>
<div class="progress-bar-bg"><div class="progress-bar-fg" style="width:${pS}%;background:${cS};"></div></div>
</div>
<div style="flex:1;">
<div style="display:flex;justify-content:space-between;font-size:12px;color:#8f8d88;">
<span>Revenus €/mois</span>
<span><input type="number" value="${r}" onchange="updateNexum('rev', this.value)" style="width:50px;background:transparent;border:none;color:#e8e6e3;text-align:right;font-family:'JetBrains Mono';font-size:12px;"> / 300</span>
</div>
<div class="progress-bar-bg"><div class="progress-bar-fg" style="width:${pR}%;background:${cR};"></div></div>
</div>
</div>
</div>`;
}
function renderMain() {
let h = '';
if (state.screen === 'home') {
let cards = '';
scopesDef.forEach(s => {
let agents = s.agents.map(a => `<span style="padding:2px 8px;border-radius:4px;border:1px solid #262626;background:#161616;font-size:10.5px;color:#a8a6a1;">${a}</span>`).join('');
cards += `<div class="hover-card" onclick="openScope('${s.id}')" style="border:1px solid #1f1f1f;border-left:3px solid ${s.color};border-radius:8px;background:#111;padding:18px 20px;cursor:pointer;display:flex;flex-direction:column;gap:14px;transition:all 0.15s;">
<div style="display:flex;align-items:center;gap:10px;">
<span style="font-family:'JetBrains Mono',monospace;font-size:15px;font-weight:500;color:#e8e6e3;">${s.label}</span>
<span style="margin-left:auto;font-family:'JetBrains Mono',monospace;font-size:13px;color:#8f8d88;">${getEntries(s.id).length} entrées</span>
</div>
<div style="font-size:13px;color:#8f8d88;line-height:1.5;">${s.desc}</div>
<div style="display:flex;align-items:center;gap:8px;padding-top:12px;border-top:1px solid #1c1c1c;">
<span style="font-size:11.5px;color:#5e5c58;">MAJ ${s.updated}</span>
<div style="margin-left:auto;display:flex;gap:5px;">${agents}</div>
</div>
</div>`;
});
const activeAgents = state.keysData.length;
const reqs = state.auditData.length;
h = `<div style="display:flex;flex-direction:column;height:100%;overflow:hidden;">
<header style="padding:22px 32px;border-bottom:1px solid #1a1a1a;flex-shrink:0;">
<div style="font-size:17px;font-weight:600;">Scopes</div>
<div style="margin-top:3px;font-size:12.5px;color:#8f8d88;">Le contenu vivant du cerveau, réparti en domaines.</div>
</header>
<div style="flex:1;overflow-y:auto;padding:26px 32px 40px 32px;">
${renderNexum()}
<div style="display:flex;gap:0;border:1px solid #1f1f1f;border-radius:8px;background:#111;margin-bottom:26px;overflow:hidden;">
<div style="flex:1;padding:16px 22px;border-right:1px solid #1c1c1c;">
<div style="font-size:11px;color:#5e5c58;letter-spacing:0.04em;">ENTRÉES DOMAINE (Infra+LLM)</div>
<div style="margin-top:6px;font-size:24px;font-weight:600;">${getEntries('infra').length+getEntries('llm').length}</div>
</div>
<div style="flex:1;padding:16px 22px;border-right:1px solid #1c1c1c;">
<div style="font-size:11px;color:#5e5c58;letter-spacing:0.04em;">AGENTS ACTIFS</div>
<div style="margin-top:6px;font-size:24px;font-weight:600;">${activeAgents}</div>
</div>
<div style="flex:1;padding:16px 22px;">
<div style="font-size:11px;color:#5e5c58;letter-spacing:0.04em;">REQUÊTES AUDIT</div>
<div style="margin-top:6px;font-size:24px;font-weight:600;">${reqs}</div>
</div>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(330px,1fr));gap:14px;">${cards}</div>
</div>
</div>`;
}
else if (state.screen === 'scope') {
const cur = scopesDef.find(s=>s.id===state.scope);
const entries = getEntries(state.scope);
let rows = entries.map(e => `
<div class="hover-bg-10" style="display:grid;grid-template-columns:170px minmax(120px,1fr) 92px 150px;min-width:540px;align-items:center;padding:13px 12px;border-bottom:1px solid #161616;font-size:13px;">
<div style="font-family:'JetBrains Mono',monospace;font-size:12.5px;color:#d8d6d1;padding-right:12px;">${e.key}</div>
<div style="font-family:'JetBrains Mono',monospace;font-size:12.5px;color:#9a9893;padding-right:12px;">${e.value}</div>
<div><span style="padding:2px 8px;border-radius:4px;font-size:11px;border:1px solid #2a3a4d;color:#8aa6c4;">${e.type}</span></div>
<div style="display:flex;gap:4px;justify-content:flex-end;">
<button class="hover-border-3a" onclick="copy('${e.value.replace(/'/g, "\'")}')" style="background:transparent;border:1px solid #242424;color:#8f8d88;font-size:11px;padding:4px 8px;border-radius:5px;cursor:pointer;">copier</button>
</div>
</div>`).join('');
if(entries.length===0) rows = `<div style="padding:20px 12px;color:#6a6863;font-size:13px;">Aucune donnée (Backend statique ou vide)</div>`;
h = `<div style="display:flex;flex-direction:column;height:100%;overflow:hidden;">
<header style="padding:20px 32px;border-bottom:1px solid #1a1a1a;flex-shrink:0;display:flex;align-items:center;gap:16px;">
<div>
<div style="display:flex;align-items:center;gap:9px;font-size:12px;color:#5e5c58;">
<span class="hover-border-3a" onclick="go('home')" style="cursor:pointer;color:#8f8d88;">Scopes</span>
<span>/</span>
<span style="display:inline-flex;align-items:center;gap:6px;color:#a8a6a1;"><span style="width:8px;height:8px;border-radius:2px;background:${cur.color};"></span></span>
</div>
<div style="margin-top:4px;display:flex;align-items:baseline;gap:10px;">
<span style="font-family:'JetBrains Mono',monospace;font-size:18px;font-weight:500;">${cur.label}</span>
<span style="font-size:12.5px;color:#8f8d88;">${cur.desc}</span>
</div>
</div>
</header>
<div style="flex:1;overflow:auto;padding:0 32px 40px 32px;">
<div style="display:grid;grid-template-columns:170px minmax(120px,1fr) 92px 150px;min-width:540px;padding:14px 12px;border-bottom:1px solid #1c1c1c;font-size:10.5px;font-weight:600;letter-spacing:0.1em;color:#4f4d49;position:sticky;top:0;background:#0d0d0d;z-index:2;">
<div>CLÉ</div><div>VALEUR</div><div>TYPE</div><div style="text-align:right;">ACTIONS</div>
</div>
${rows}
</div>
</div>`;
}
else if (state.screen === 'ports') {
const explicitFree = [3091, 3001, 3002, 3015];
const allPorts = new Set(explicitFree);
for(let i=3100; i<=3199; i++) allPorts.add(i);
Object.keys(state.portsData).forEach(p => allPorts.add(parseInt(p)));
const sorted = Array.from(allPorts).sort((a,b)=>a-b);
let chips = '';
sorted.forEach(p => {
let serv = state.portsData[p];
if(serv) {
chips += `<div style="padding:6px 12px;border-radius:6px;font-size:12px;background:#1a0f0e;border:1px solid #5a352f;color:#c47a6e;font-family:'JetBrains Mono';display:flex;align-items:center;gap:6px;"><div style="width:4px;height:4px;border-radius:50%;background:#d97757;animation:pulse 1s infinite;"></div>${p} <span style="font-size:10px;opacity:0.6;font-family:sans-serif;">${serv}</span></div>`;
} else {
chips += `<div style="padding:6px 12px;border-radius:6px;font-size:12px;background:transparent;border:1px solid #222;color:#6a6863;font-family:'JetBrains Mono';">${p}</div>`;
}
});
h = `<div style="display:flex;flex-direction:column;height:100%;overflow:hidden;">
<header style="padding:22px 32px;border-bottom:1px solid #1a1a1a;flex-shrink:0;">
<div style="font-size:17px;font-weight:600;">Ports Réseau</div>
<div style="margin-top:3px;font-size:12.5px;color:#8f8d88;">Surveillance des ports ouverts sur context.bolbol.tn.</div>
</header>
<div style="flex:1;overflow-y:auto;padding:26px 32px 40px 32px;">
<div style="display:flex;flex-wrap:wrap;gap:8px;">${chips}</div>
</div>
</div>`;
}
else if (state.screen === 'keys') {
let rows = state.keysData.map((k, idx) => `
<div class="hover-bg-10" style="display:grid;grid-template-columns:minmax(150px,1fr) 200px 100px;min-width:450px;align-items:center;padding:14px 12px;border-bottom:1px solid #161616;font-size:13px;">
<div style="font-weight:500;color:${agentColor('Claude')};">Agent ${idx+1}</div>
<div style="display:flex;align-items:center;gap:8px;">
<span style="font-family:'JetBrains Mono',monospace;font-size:12px;color:#9a9893;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${k.api_key}</span>
</div>
<div style="display:flex;gap:5px;justify-content:flex-end;">
<button class="hover-border-3a" onclick="copy('${k.api_key}')" style="background:transparent;border:1px solid #242424;color:#8f8d88;font-size:11px;padding:4px 8px;border-radius:5px;cursor:pointer;">copier</button>
</div>
</div>`).join('');
h = `<div style="display:flex;flex-direction:column;height:100%;overflow:hidden;">
<header style="padding:22px 32px;border-bottom:1px solid #1a1a1a;flex-shrink:0;">
<div style="font-size:17px;font-weight:600;">Clés API</div>
<div style="margin-top:3px;font-size:12.5px;color:#8f8d88;">Clés générées depuis la base de données de context-hub.</div>
</header>
<div style="flex:1;overflow:auto;padding:0 32px 40px 32px;">
<div style="display:grid;grid-template-columns:minmax(150px,1fr) 200px 100px;min-width:450px;padding:14px 12px;border-bottom:1px solid #1c1c1c;font-size:10.5px;font-weight:600;letter-spacing:0.1em;color:#4f4d49;">
<div>AGENT</div><div>CLÉ</div><div style="text-align:right;">ACTIONS</div>
</div>
${rows}
</div>
</div>`;
}
else if (state.screen === 'activity') {
const actMeta=(a)=> a.includes('écrit') ? {color:'#d99a86',border:'#5a352f'} : a.includes('refus') ? {color:'#c47a6e',border:'#5a352f'} : {color:'#7fae93',border:'#243f30'};
let rows = state.auditData.filter(r=>state.filter==='all'||r.agent_name.includes(state.filter)||r.scope===state.filter).map(r=>{
const am = actMeta(r.action.toLowerCase());
const dt = new Date(r.timestamp);
const time = dt.toLocaleTimeString('fr-FR', {hour:'2-digit', minute:'2-digit', second:'2-digit'});
const sc = r.scope || 'infra';
return `<div class="hover-bg-10" style="display:grid;grid-template-columns:96px 120px 120px minmax(140px,1fr) 104px;min-width:580px;align-items:center;padding:12px 12px;border-bottom:1px solid #161616;font-size:13px;">
<div style="font-family:'JetBrains Mono',monospace;font-size:12px;color:#6a6863;">${time}</div>
<div style="font-weight:500;color:${agentColor(r.agent_name)};">${r.agent_name}</div>
<div style="display:flex;align-items:center;gap:7px;"><span style="width:7px;height:7px;border-radius:2px;background:${colorOf(sc)};"></span><span style="font-family:'JetBrains Mono',monospace;font-size:12px;color:#a8a6a1;">${sc}</span></div>
<div style="font-family:'JetBrains Mono',monospace;font-size:12px;color:#9a9893;">${r.action}</div>
<div style="text-align:right;"><span style="padding:2px 9px;border-radius:4px;font-size:11px;border:1px solid ${am.border};color:${am.color};">${r.action}</span></div>
</div>`;
}).join('');
const chips = [{l:'Tous',v:'all'},{l:'Claude',v:'Claude'},{l:'Gemini',v:'Gemini'},{l:'infra',v:'infra'},{l:'llm',v:'llm'},{l:'nyora',v:'nyora'},{l:'perso',v:'perso'},{l:'tt',v:'tt'}].map(c=>{
const a = state.filter===c.v;
const style = `padding:5px 12px;border-radius:6px;font-size:12px;cursor:pointer;border:1px solid ${a?'#3a3a3a':'#222'};background:${a?'#1c1c1c':'transparent'};color:${a?'#e8e6e3':'#8f8d88'};`;
return `<button class="hover-border-3a" onclick="setFilter('${c.v}')" style="${style}">${c.l}</button>`;
}).join('');
h = `<div style="display:flex;flex-direction:column;height:100%;overflow:hidden;">
<header style="padding:22px 32px;border-bottom:1px solid #1a1a1a;flex-shrink:0;">
<div style="display:flex;align-items:center;gap:10px;">
<div style="font-size:17px;font-weight:600;">Activité</div>
<div style="display:flex;align-items:center;gap:6px;padding:3px 9px;border-radius:5px;background:#13201a;border:1px solid #1f3329;">
<div style="width:6px;height:6px;border-radius:50%;background:#6fa888;animation:pulse 2.4s ease-in-out infinite;"></div>
<span style="font-size:11px;color:#7fae93;">temps réel</span>
</div>
</div>
<div style="margin-top:12px;display:flex;flex-wrap:wrap;gap:6px;">${chips}</div>
</header>
<div style="flex:1;overflow-y:auto;padding:6px 32px 40px 32px;">${rows}</div>
</div>`;
}
document.getElementById('main-content').innerHTML = h;
}
function render() {
renderSidebar();
renderMain();
}
// FETCH DATA
async function fetchData() {
try {
const r = await fetch('/api/admin/all');
const d = await r.json();
state.scopeData = d.scopes || {};
state.infraData = d.scopes ? (d.scopes.infra || {}) : {};
state.llmData = d.scopes ? (d.scopes.llm || {}) : {};
state.portsData = {}; if(Array.isArray(d.ports)) d.ports.forEach(p => { state.portsData[p.port] = p; });
state.keysData = Array.isArray(d.keys) ? d.keys : [];
state.auditData = Array.isArray(d.audit) ? d.audit.slice(0,100) : [];
render();
} catch(e) {
console.error(e);
flash('Erreur de chargement');
render();
}
}
document.addEventListener('DOMContentLoaded', () => {
fetchData();
});
</script>
{% endblock %}