Files
wevia-brain/s89-arsenal-screens/kb-sync-monitor.html
2026-04-12 23:01:36 +02:00

447 lines
20 KiB
HTML
Executable File

<?php include_once("/opt/wevads-arsenal/public/api/wevads-metrics.php"); ?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KB Sync Monitor - WEVADS</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=JetBrains+Mono:wght@400;500;700&display=swap');
:root {
--bg: #0c0e14; --surface: #151821; --surface2: #1c2030; --border: #262a3a;
--text: #e4e6f0; --text2: #8b90a8; --text3: #555a72;
--cyan: #00e5ff; --green: #10b981; --amber: #f59e0b; --red: #ef4444;
--purple: #8b5cf6; --blue: #3b82f6; --pink: #ec4899;
--gradient1: linear-gradient(135deg, #00e5ff22, #8b5cf622);
}
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:'DM Sans',sans-serif; background:var(--bg); color:var(--text); min-height:100vh; }
.mono { font-family:'JetBrains Mono',monospace; }
/* Topbar */
.topbar {
background:var(--surface); border-bottom:1px solid var(--border); padding:14px 28px;
display:flex; align-items:center; justify-content:space-between; position:sticky; top:0; z-index:50;
}
.topbar h1 { font-size:18px; font-weight:700; display:flex; align-items:center; gap:10px; }
.topbar h1 i { color:var(--cyan); font-size:20px; }
.topbar-right { display:flex; align-items:center; gap:14px; }
.sync-btn {
background:linear-gradient(135deg, var(--cyan), var(--blue)); border:none; color:#000;
padding:8px 18px; border-radius:8px; font-weight:700; font-size:13px; cursor:pointer;
font-family:inherit; transition:all .2s; display:flex; align-items:center; gap:6px;
}
.sync-btn:hover { transform:translateY(-1px); box-shadow:0 4px 20px #00e5ff44; }
.sync-btn.running { opacity:.6; pointer-events:none; }
.last-sync { font-size:11px; color:var(--text3); }
/* Layout */
.container { max-width:1440px; margin:0 auto; padding:24px; }
/* Stats Grid */
.stats { display:grid; grid-template-columns:repeat(6,1fr); gap:14px; margin-bottom:28px; }
.stat {
background:var(--surface); border:1px solid var(--border); border-radius:12px; padding:18px;
position:relative; overflow:hidden; transition:all .3s;
}
.stat:hover { border-color:var(--cyan); transform:translateY(-2px); }
.stat::before {
content:''; position:absolute; top:0; left:0; right:0; height:3px;
background:var(--gradient1); opacity:0; transition:opacity .3s;
}
.stat:hover::before { opacity:1; }
.stat-value { font-size:32px; font-weight:700; line-height:1; }
.stat-label { font-size:11px; color:var(--text2); text-transform:uppercase; letter-spacing:1px; margin-top:6px; }
.stat-icon { position:absolute; top:14px; right:14px; font-size:16px; opacity:.3; }
/* Pipeline Visual */
.pipeline {
background:var(--surface); border:1px solid var(--border); border-radius:14px;
padding:24px; margin-bottom:28px;
}
.pipeline-title { font-size:14px; font-weight:700; margin-bottom:18px; color:var(--text2); text-transform:uppercase; letter-spacing:1px; }
.pipeline-flow { display:flex; align-items:center; justify-content:space-between; gap:8px; }
.pipeline-node {
flex:1; background:var(--surface2); border:1px solid var(--border); border-radius:10px;
padding:16px; text-align:center; position:relative; transition:all .3s;
}
.pipeline-node:hover { border-color:var(--cyan); }
.pipeline-node .p-icon { font-size:24px; margin-bottom:8px; }
.pipeline-node .p-name { font-size:12px; font-weight:700; color:var(--text); }
.pipeline-node .p-count { font-size:22px; font-weight:700; margin-top:4px; }
.pipeline-node .p-table { font-size:9px; color:var(--text3); margin-top:2px; font-family:'JetBrains Mono',monospace; }
.pipeline-arrow { color:var(--cyan); font-size:18px; flex-shrink:0; animation:pulse-arrow 2s infinite; }
@keyframes pulse-arrow { 0%,100%{opacity:.3} 50%{opacity:1} }
/* Tabs */
.tabs { display:flex; gap:2px; margin-bottom:20px; background:var(--surface); border-radius:10px; padding:3px; border:1px solid var(--border); width:fit-content; }
.tab {
padding:8px 18px; border-radius:8px; font-size:13px; font-weight:600; cursor:pointer;
color:var(--text2); transition:all .2s; display:flex; align-items:center; gap:6px; border:none; background:none; font-family:inherit;
}
.tab:hover { color:var(--text); }
.tab.active { background:var(--cyan); color:#000; }
.tab .badge {
font-size:10px; background:var(--surface2); color:var(--text2); padding:2px 7px;
border-radius:10px; font-weight:700;
}
.tab.active .badge { background:rgba(0,0,0,.2); color:#000; }
/* Table */
.table-wrap { background:var(--surface); border:1px solid var(--border); border-radius:14px; overflow:hidden; }
.table-header { padding:16px 20px; border-bottom:1px solid var(--border); display:flex; align-items:center; justify-content:space-between; }
.table-header h3 { font-size:14px; font-weight:700; }
.search-box {
background:var(--surface2); border:1px solid var(--border); border-radius:8px; padding:7px 14px;
color:var(--text); font-size:13px; font-family:inherit; width:280px; outline:none; transition:border .2s;
}
.search-box:focus { border-color:var(--cyan); }
table { width:100%; border-collapse:collapse; }
thead th {
padding:10px 16px; font-size:11px; color:var(--text3); text-transform:uppercase; letter-spacing:.5px;
text-align:left; border-bottom:1px solid var(--border); font-weight:600; cursor:pointer;
}
thead th:hover { color:var(--text2); }
tbody tr { border-bottom:1px solid var(--border); transition:background .15s; }
tbody tr:hover { background:var(--surface2); }
tbody tr:last-child { border-bottom:none; }
td { padding:12px 16px; font-size:13px; }
td.id { font-family:'JetBrains Mono',monospace; font-size:11px; color:var(--text3); }
td.title-cell { font-weight:500; max-width:300px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
td.content-cell { max-width:280px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--text2); font-size:12px; }
td.date { font-family:'JetBrains Mono',monospace; font-size:11px; color:var(--text3); }
.cat-badge {
display:inline-block; padding:2px 8px; border-radius:4px; font-size:10px; font-weight:700;
text-transform:uppercase; letter-spacing:.5px;
}
.cat-infra { background:#00e5ff22; color:var(--cyan); }
.cat-frame { background:#8b5cf622; color:var(--purple); }
.cat-ops { background:#10b98122; color:var(--green); }
.cat-monitor { background:#f59e0b22; color:var(--amber); }
.cat-default { background:#3b82f622; color:var(--blue); }
.type-badge {
display:inline-flex; align-items:center; gap:4px; padding:3px 8px; border-radius:5px;
font-size:10px; font-weight:700; text-transform:uppercase;
}
.type-conv { background:#8b5cf622; color:var(--purple); }
.type-kb { background:#10b98122; color:var(--green); }
.type-comm { background:#00e5ff22; color:var(--cyan); }
.type-file { background:#f59e0b22; color:var(--amber); }
.prio {
display:inline-flex; align-items:center; justify-content:center; width:24px; height:24px;
border-radius:6px; font-size:11px; font-weight:700;
}
.prio-high { background:#ef444422; color:var(--red); }
.prio-med { background:#f59e0b22; color:var(--amber); }
.prio-low { background:#10b98122; color:var(--green); }
.expand-btn {
background:var(--surface2); border:1px solid var(--border); border-radius:6px;
padding:4px 8px; cursor:pointer; color:var(--text2); font-size:11px; transition:all .2s;
}
.expand-btn:hover { border-color:var(--cyan); color:var(--cyan); }
/* Cron Status */
.cron-status { display:flex; align-items:center; gap:8px; }
.cron-dot { width:8px; height:8px; border-radius:50%; }
.cron-dot.active { background:var(--green); box-shadow:0 0 8px var(--green); }
.cron-dot.dead { background:var(--red); box-shadow:0 0 8px var(--red); }
/* Detail Panel */
.detail-overlay { display:none; position:fixed; inset:0; background:rgba(0,0,0,.7); z-index:100; }
.detail-panel {
position:fixed; top:0; right:0; bottom:0; width:560px; background:var(--surface); z-index:101;
border-left:1px solid var(--border); overflow-y:auto; padding:28px; transform:translateX(100%);
transition:transform .3s ease;
}
.detail-panel.open { transform:translateX(0); }
.detail-close { background:none; border:none; color:var(--text2); font-size:20px; cursor:pointer; float:right; }
.detail-title { font-size:18px; font-weight:700; margin-bottom:16px; padding-right:40px; }
.detail-meta { display:flex; flex-wrap:wrap; gap:8px; margin-bottom:16px; }
.detail-content {
background:var(--surface2); border:1px solid var(--border); border-radius:8px; padding:16px;
font-size:13px; line-height:1.7; color:var(--text2); white-space:pre-wrap; max-height:60vh; overflow-y:auto;
}
/* Empty state */
.empty { text-align:center; padding:60px 20px; color:var(--text3); }
.empty i { font-size:40px; margin-bottom:12px; display:block; }
/* Log */
.log-box {
background:var(--bg); border:1px solid var(--border); border-radius:8px; padding:14px;
font-family:'JetBrains Mono',monospace; font-size:11px; color:var(--green); max-height:200px;
overflow-y:auto; margin-top:20px; display:none; white-space:pre-wrap;
}
@media (max-width:900px) {
.stats { grid-template-columns:repeat(3,1fr); }
.pipeline-flow { flex-wrap:wrap; }
}
</style>
<link rel="stylesheet" href="wevads-global.css?v1770777318">
</head>
<body>
<div class="topbar">
<h1><i class="fas fa-brain"></i> KB Sync Monitor</h1>
<div class="topbar-right">
<div class="cron-status">
<span class="cron-dot" id="cron-dot"></span>
<span style="font-size:11px;color:var(--text3)" id="cron-label">Checking cron...</span>
</div>
<span class="last-sync" id="last-sync"></span>
<button class="sync-btn" id="sync-btn" onclick="runSync()"><i class="fas fa-sync-alt"></i> Sync Now</button>
</div>
</div>
<div class="container">
<!-- Stats -->
<div class="stats" id="stats-grid"></div>
<!-- Pipeline -->
<div class="pipeline">
<div class="pipeline-title"><i class="fas fa-diagram-project"></i> Sync Pipeline</div>
<div class="pipeline-flow" id="pipeline-flow"></div>
</div>
<!-- Tabs -->
<div class="tabs" id="tabs"></div>
<!-- Table -->
<div class="table-wrap">
<div class="table-header">
<h3 id="table-title">Loading...</h3>
<input type="text" class="search-box" id="search" placeholder="Rechercher..." oninput="filterTable()">
</div>
<table>
<thead id="thead"></thead>
<tbody id="tbody"></tbody>
</table>
</div>
<div class="log-box" id="log-box"></div>
</div>
<!-- Detail Panel -->
<div class="detail-overlay" id="detail-overlay" onclick="closeDetail()"></div>
<div class="detail-panel" id="detail-panel">
<button class="detail-close" onclick="closeDetail()"><i class="fas fa-times"></i></button>
<div id="detail-body"></div>
</div>
<script>
const API = '/kb-sync-monitor-api.php';
let allData = {};
let currentTab = 'conversations';
async function load() {
try {
const r = await fetch(API + '?action=full_status');
allData = await r.json();
renderStats();
renderPipeline();
renderTabs();
renderTable();
document.getElementById('last-sync').textContent = 'Last sync: ' + (allData.last_sync || 'Never');
const dot = document.getElementById('cron-dot');
const label = document.getElementById('cron-label');
if (allData.cron_active) { dot.className='cron-dot active'; label.textContent='Cron ✓ every 2h'; }
else { dot.className='cron-dot dead'; label.textContent='Cron OFF'; }
} catch(e) { console.error(e); }
}
function renderStats() {
const s = allData.stats || {};
const items = [
{v:s.total_kb||0, l:'Knowledge Base', icon:'fa-book', color:'var(--cyan)'},
{v:s.total_conversations||0, l:'Claude Conversations', icon:'fa-comments', color:'var(--purple)'},
{v:s.total_commonia||0, l:'Commonia Synced', icon:'fa-share-nodes', color:'var(--green)'},
{v:s.total_files||0, l:'KB Files (.md)', icon:'fa-file-lines', color:'var(--amber)'},
{v:s.categories||0, l:'Categories', icon:'fa-tags', color:'var(--blue)'},
{v:s.today_synced||0, l:'Synced Today', icon:'fa-clock-rotate-left', color:'var(--pink)'},
];
document.getElementById('stats-grid').innerHTML = items.map(i => `
<div class="stat">
<div class="stat-value" style="color:${i.color}">${i.v}</div>
<div class="stat-label">${i.l}</div>
<i class="fas ${i.icon} stat-icon" style="color:${i.color}"></i>
</div>`).join('');
}
function renderPipeline() {
const p = allData.pipeline || {};
const nodes = [
{icon:'💬', name:'Claude Sessions', count:p.sessions||'—', table:'conversations'},
{icon:'📄', name:'KB Files', count:p.files||'—', table:'hamid-files/kb_*.md'},
{icon:'📚', name:'Knowledge Base', count:p.knowledge_base||'—', table:'knowledge_base'},
{icon:'🧠', name:'Claude Conv KB', count:p.conversations_kb||'—', table:'claude_conversations_kb'},
{icon:'🌐', name:'Commonia', count:p.commonia||'—', table:'commonia_knowledge'},
];
document.getElementById('pipeline-flow').innerHTML = nodes.map((n,i) =>
`<div class="pipeline-node"><div class="p-icon">${n.icon}</div><div class="p-name">${n.name}</div><div class="p-count">${n.count}</div><div class="p-table">${n.table}</div></div>` +
(i < nodes.length-1 ? '<i class="fas fa-chevron-right pipeline-arrow"></i>' : '')
).join('');
}
function renderTabs() {
const tabs = [
{id:'conversations', label:'Claude Conversations', count:allData.stats?.total_conversations||0, icon:'fa-comments'},
{id:'knowledge', label:'Knowledge Base', count:allData.stats?.total_kb||0, icon:'fa-book'},
{id:'commonia', label:'Commonia', count:allData.stats?.total_commonia||0, icon:'fa-share-nodes'},
{id:'files', label:'KB Files', count:allData.stats?.total_files||0, icon:'fa-file-lines'},
{id:'log', label:'Sync Log', count:'', icon:'fa-terminal'},
];
document.getElementById('tabs').innerHTML = tabs.map(t =>
`<button class="tab ${t.id===currentTab?'active':''}" onclick="switchTab('${t.id}')"><i class="fas ${t.icon}"></i> ${t.label} ${t.count!==''?`<span class="badge">${t.count}</span>`:''}</button>`
).join('');
}
function switchTab(tab) { currentTab = tab; renderTabs(); renderTable(); }
function renderTable() {
const search = document.getElementById('search').value.toLowerCase();
let rows = allData[currentTab] || [];
if (search) rows = rows.filter(r => JSON.stringify(r).toLowerCase().includes(search));
const title = document.getElementById('table-title');
const thead = document.getElementById('thead');
const tbody = document.getElementById('tbody');
const logBox = document.getElementById('log-box');
logBox.style.display = 'none';
if (currentTab === 'log') {
title.textContent = 'Sync Log';
thead.innerHTML = ''; tbody.innerHTML = '';
logBox.style.display = 'block';
logBox.textContent = allData.sync_log || 'No log available';
return;
}
if (currentTab === 'conversations') {
title.textContent = `Claude Conversations (${rows.length})`;
thead.innerHTML = '<tr><th>ID</th><th>Title</th><th>Category</th><th>Summary</th><th>Score</th><th>Synced</th><th>Created</th><th></th></tr>';
tbody.innerHTML = rows.map(r => `<tr>
<td class="id">#${r.id}</td>
<td class="title-cell">${esc(r.title)}</td>
<td>${catBadge(r.category)}</td>
<td class="content-cell">${esc(r.summary||'')}</td>
<td>${prioBadge(r.importance_score)}</td>
<td class="date">${r.last_synced_at ? shortDate(r.last_synced_at) : '—'}</td>
<td class="date">${shortDate(r.created_at)}</td>
<td><button class="expand-btn" onclick='showDetail(${JSON.stringify(r).replace(/'/g,"&#39;")})'>👁</button></td>
</tr>`).join('');
}
else if (currentTab === 'knowledge') {
title.textContent = `Knowledge Base (${rows.length})`;
thead.innerHTML = '<tr><th>ID</th><th>Title</th><th>Category</th><th>Content</th><th>Source</th><th>Created</th><th></th></tr>';
tbody.innerHTML = rows.map(r => `<tr>
<td class="id">#${r.id}</td>
<td class="title-cell">${esc(r.title)}</td>
<td>${catBadge(r.category)}</td>
<td class="content-cell">${esc((r.content||'').substring(0,100))}</td>
<td class="date">${esc(r.source||'—')}</td>
<td class="date">${shortDate(r.created_at)}</td>
<td><button class="expand-btn" onclick='showDetail(${JSON.stringify(r).replace(/'/g,"&#39;")})'>👁</button></td>
</tr>`).join('');
}
else if (currentTab === 'commonia') {
title.textContent = `Commonia Knowledge (${rows.length})`;
thead.innerHTML = '<tr><th>ID</th><th>Type</th><th>Title</th><th>Category</th><th>Content</th><th>Prio</th><th>Created</th><th></th></tr>';
tbody.innerHTML = rows.map(r => `<tr>
<td class="id">#${r.id}</td>
<td>${typeBadge(r.type)}</td>
<td class="title-cell">${esc(r.title||r.question||'')}</td>
<td>${catBadge(r.category)}</td>
<td class="content-cell">${esc((r.content||r.answer||'').substring(0,80))}</td>
<td>${prioBadge(r.priority)}</td>
<td class="date">${shortDate(r.created_at)}</td>
<td><button class="expand-btn" onclick='showDetail(${JSON.stringify(r).replace(/'/g,"&#39;")})'>👁</button></td>
</tr>`).join('');
}
else if (currentTab === 'files') {
title.textContent = `KB Files (${rows.length})`;
thead.innerHTML = '<tr><th>File</th><th>Size</th><th>Title</th><th>Modified</th></tr>';
tbody.innerHTML = rows.map(r => `<tr>
<td><span class="mono" style="font-size:12px;color:var(--amber)">${esc(r.name)}</span></td>
<td class="date">${r.size}</td>
<td class="title-cell">${esc(r.title||'')}</td>
<td class="date">${r.modified||'—'}</td>
</tr>`).join('');
}
}
function filterTable() { renderTable(); }
function showDetail(data) {
const body = document.getElementById('detail-body');
const content = data.content || data.summary || data.answer || '(empty)';
body.innerHTML = `
<div class="detail-title">${esc(data.title||data.question||data.name||'Detail')}</div>
<div class="detail-meta">
${data.category ? catBadge(data.category) : ''}
${data.type ? typeBadge(data.type) : ''}
${data.importance_score ? prioBadge(data.importance_score) : ''}
${data.created_at ? `<span style="font-size:11px;color:var(--text3)"><i class="far fa-clock"></i> ${data.created_at}</span>` : ''}
</div>
<div class="detail-content">${esc(content)}</div>`;
document.getElementById('detail-overlay').style.display = 'block';
document.getElementById('detail-panel').classList.add('open');
}
function closeDetail() {
document.getElementById('detail-overlay').style.display = 'none';
document.getElementById('detail-panel').classList.remove('open');
}
async function runSync() {
const btn = document.getElementById('sync-btn');
btn.classList.add('running'); btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Syncing...';
try {
const r = await fetch(API + '?action=sync');
const d = await r.json();
const logBox = document.getElementById('log-box');
logBox.style.display = 'block';
logBox.textContent = JSON.stringify(d, null, 2);
await load();
} catch(e) { alert('Sync error: ' + e.message); }
btn.classList.remove('running'); btn.innerHTML = '<i class="fas fa-sync-alt"></i> Sync Now';
}
function esc(s) { if(!s)return''; const d=document.createElement('div'); d.textContent=s; return d.innerHTML; }
function shortDate(d) { if(!d)return'—'; return d.replace('T',' ').substring(0,16); }
function catBadge(c) {
if(!c)return'';
const cl = c.toLowerCase();
let cls = 'cat-default';
if(cl.includes('infra')||cl.includes('server')) cls='cat-infra';
else if(cl.includes('frame')||cl.includes('adx')) cls='cat-frame';
else if(cl.includes('oper')||cl.includes('method')) cls='cat-ops';
else if(cl.includes('monitor')||cl.includes('affil')) cls='cat-monitor';
return `<span class="cat-badge ${cls}">${esc(c)}</span>`;
}
function typeBadge(t) {
if(!t)return'';
const m = {claude_session:'type-conv',qa:'type-kb',doc:'type-file',discussion:'type-comm'};
return `<span class="type-badge ${m[t]||'type-kb'}">${esc(t)}</span>`;
}
function prioBadge(p) {
if(!p&&p!==0)return'—';
const cls = p>=9?'prio-high':p>=7?'prio-med':'prio-low';
return `<span class="prio ${cls}">${p}</span>`;
}
load();
setInterval(load, 60000);
</script>
<script src="arsenal-common.js?v1770778169"></script>
<?php include("/opt/wevads-arsenal/public/universal-drill.html"); ?>
</body>
</html>