Files
pgz-sport/static/kpi.html
T
Damir Radulić 8dce58c5f9 CC3: Unified sidebar with external portal links + collapsible icon mode
Shared module:
- /static/shared/sidebar.css   ← unified CSS (#pgz-sb, .pgz-collapsed, mobile overlay, tooltip)
- /static/shared/sidebar.js    ← auto-mounting JS shell + PGZSidebar API
   * Auto-renders #pgz-sb na <body> start (data-inline=1 to opt out)
   * NAV_EXTERNAL: Prijava, Aplikacija, Administracija, CRM, ERP, KPI, Audit, Public portal
   * Toggle (≡) -> localStorage 'sidebarCollapsed' (perzistira preko SVIH stranica)
   * Mobile <768px: ≡ burger + ✕ close, body backdrop
   * Loads /api/auth/me u footer (avatar/username/uloga); ⎋ logout briše JWT i ide na /login
   * data-active="<key>" highlight aktivnog portala

Page integration:
- sport2.html  ← inline NAV_EXTERNAL u buildNav() + "Portali" separator (zadrži postojeći sidebar)
- app.html     ← inline NAV_EXTERNAL u buildNav() (zadrži role-based interni nav, dopuni Portalima)
- admin.html   ← Portali stavke u <aside class="sidebar"> (matching .nav-item style)
- erp.html     ← Portali stavke u <aside class="sidebar"> (matching .nav-item style)
- crm.html     ← include shared sidebar.css + sidebar.js  data-active="crm"
- audit.html   ← include shared sidebar.css + sidebar.js  data-active="audit"
- kpi.html     ← include shared sidebar.css + sidebar.js  data-active="kpi"
- login.html   ← include shared sidebar.css + sidebar.js  data-active="login"

Backups: _backups/{*.cc3_pre_unified_sidebar.*}

Live verified: 8 pages serve HTTP 200; sidebar.css/js HTTP 200; portal markers per page OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 01:11:24 +02:00

103 lines
5.5 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="/sport/static/shared/sidebar.css">
<script src="/sport/static/shared/sidebar.js" defer data-active="kpi"></script>
</head>
<body>
<h1>RINET KPI Dashboard <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>