4fc8327789
Orchestrator-side: - routers/img_proxy_router.py: 4xx/5xx → 1x1 transparent PNG (eliminates cascade <img onerror>) - static/sport2.html: removed standalone three.min.js (3d-force-graph bundles), bumped to 1.73.4 CC3 (before limit hit): - Logo home link applied to ALL HTML pages (admin.html, admin_users.html, audit.html, crm.html, erp.html, kpi.html, login.html) - Backups in _backups/*.cc3_pre_logo.$ts CC4 R3 (before plan mode): - _backups/r3_cc4/ocr.py.pre_S2.$ts Audit screenshots (80 pages) committed to _audit/audit_20260505_023639/shots/
103 lines
5.6 KiB
HTML
103 lines
5.6 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="hr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>RINET KPI Dashboard</title>
|
|
<style>
|
|
body { font-family: -apple-system, sans-serif; background: #0a0e1a; color: #d0d8e8; margin: 0; padding: 20px; }
|
|
h1 { color: #4af; margin: 0 0 20px; font-size: 24px; }
|
|
h2 { color: #6cf; margin: 20px 0 8px; font-size: 16px; border-bottom: 1px solid #2a3a4a; padding-bottom: 4px; }
|
|
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 12px; margin-bottom: 16px; }
|
|
.card { background: #14192a; padding: 12px 16px; border-radius: 6px; border-left: 3px solid #4af; }
|
|
.card .label { color: #88a; font-size: 11px; text-transform: uppercase; }
|
|
.card .value { color: #fff; font-size: 22px; font-weight: bold; margin: 4px 0; }
|
|
.card .sub { color: #aab; font-size: 12px; }
|
|
.card.good { border-left-color: #4f4; }
|
|
.card.warn { border-left-color: #fa4; }
|
|
.card.bad { border-left-color: #f44; }
|
|
table { width: 100%; border-collapse: collapse; }
|
|
th, td { text-align: left; padding: 6px 12px; border-bottom: 1px solid #2a3a4a; font-size: 12px; }
|
|
th { color: #6cf; font-weight: normal; text-transform: uppercase; font-size: 10px; }
|
|
tr:hover { background: #1a2030; }
|
|
.updated { color: #678; font-size: 11px; }
|
|
.refresh { background: #4af; color: #fff; border: none; padding: 4px 12px; border-radius: 4px; cursor: pointer; }
|
|
body{padding:20px}
|
|
</style>
|
|
<link rel="stylesheet" href="/static/shared/sidebar.css">
|
|
<script src="/static/shared/sidebar.js" defer data-active="kpi"></script>
|
|
</head>
|
|
<body>
|
|
<h1><a href="/" style="text-decoration:none;color:inherit" title="Početna">RINET KPI Dashboard</a> <span class="updated" id="updated"></span> <button class="refresh" onclick="load()">↻</button></h1>
|
|
<div id="root">Loading...</div>
|
|
|
|
<script>
|
|
async function load() {
|
|
document.getElementById('updated').textContent = '...';
|
|
try {
|
|
const r = await fetch('/admin/api/kpi');
|
|
const d = await r.json();
|
|
|
|
if (d.error) {
|
|
document.getElementById('root').innerHTML = '<div class="card bad">Error: ' + d.error + '</div>';
|
|
return;
|
|
}
|
|
|
|
const haluClass = d.queries.halu_pct > 5 ? 'bad' : d.queries.halu_pct > 1 ? 'warn' : 'good';
|
|
const clusterTotal = Object.values(d.cluster).reduce((a,b)=>a+b, 0);
|
|
const clusterUnhealthy = Object.entries(d.cluster).filter(([s,n]) => !['healthy','skipped'].includes(s)).reduce((a,[s,n])=>a+n, 0);
|
|
const clusterClass = clusterUnhealthy > 0 ? 'bad' : 'good';
|
|
const incClass = d.open_incidents > 0 ? 'warn' : 'good';
|
|
const embClass = d.knowledge.embed_pct >= 99 ? 'good' : d.knowledge.embed_pct >= 95 ? 'warn' : 'bad';
|
|
|
|
let html = `
|
|
<h2>Queries (Production)</h2>
|
|
<div class="grid">
|
|
<div class="card good"><div class="label">Last 1h</div><div class="value">${d.queries.h1}</div></div>
|
|
<div class="card good"><div class="label">Last 24h</div><div class="value">${d.queries.h24}</div></div>
|
|
<div class="card ${haluClass}"><div class="label">Halucinacije 24h</div><div class="value">${d.queries.halucinacije_h24}</div><div class="sub">${d.queries.halu_pct}%</div></div>
|
|
<div class="card good"><div class="label">Avg latency</div><div class="value">${d.queries.avg_latency_sec}s</div></div>
|
|
<div class="card good"><div class="label">Avg confidence</div><div class="value">${d.queries.avg_confidence}</div></div>
|
|
</div>
|
|
|
|
<h2>Knowledge Base</h2>
|
|
<div class="grid">
|
|
<div class="card good"><div class="label">Total facts</div><div class="value">${d.knowledge.total.toLocaleString()}</div></div>
|
|
<div class="card good"><div class="label">Added 1h / 24h</div><div class="value">+${d.knowledge.added_h1} / +${d.knowledge.added_h24}</div></div>
|
|
<div class="card ${embClass}"><div class="label">Embed coverage</div><div class="value">${d.knowledge.embed_pct}%</div><div class="sub">${d.knowledge.embed_pending} pending</div></div>
|
|
<div class="card good"><div class="label">Training Q&A</div><div class="value">${d.training.total.toLocaleString()}</div><div class="sub">+${d.training.added_h24} / 24h, ${d.training.from_capture} from capture</div></div>
|
|
</div>
|
|
|
|
<h2>Cluster Health</h2>
|
|
<div class="grid">
|
|
<div class="card ${clusterClass}"><div class="label">Healthy</div><div class="value">${d.cluster.healthy || 0} / ${clusterTotal}</div></div>
|
|
<div class="card ${incClass}"><div class="label">Open incidents</div><div class="value">${d.open_incidents}</div></div>
|
|
<div class="card good"><div class="label">Skipped</div><div class="value">${d.cluster.skipped || 0}</div><div class="sub">PG/Redis/cold by design</div></div>
|
|
<div class="card ${clusterUnhealthy>0?'bad':'good'}"><div class="label">Unhealthy</div><div class="value">${clusterUnhealthy}</div></div>
|
|
</div>
|
|
|
|
<h2>Top Sources (24h scrape)</h2>
|
|
<table>
|
|
<tr><th>Source</th><th>Count</th></tr>
|
|
${d.top_sources_h24.map(s => `<tr><td>${s.source}</td><td>${s.count.toLocaleString()}</td></tr>`).join('')}
|
|
</table>
|
|
|
|
<h2>Top Models (24h)</h2>
|
|
<table>
|
|
<tr><th>Model</th><th>Calls</th><th>Avg latency</th></tr>
|
|
${d.top_models_h24.map(m => `<tr><td>${m.model || '-'}</td><td>${m.count}</td><td>${m.avg_latency}s</td></tr>`).join('')}
|
|
</table>
|
|
`;
|
|
|
|
document.getElementById('root').innerHTML = html;
|
|
document.getElementById('updated').textContent = new Date().toLocaleTimeString();
|
|
} catch (e) {
|
|
document.getElementById('root').innerHTML = '<div class="card bad">Network error: ' + e.message + '</div>';
|
|
}
|
|
}
|
|
|
|
load();
|
|
setInterval(load, 30000); // 30s refresh
|
|
</script>
|
|
</body>
|
|
</html>
|