Package portal and close UI API gaps

This commit is contained in:
Jino Jose committed 2026-06-30 10:51:41 +05:30
1 parent e71b069014
commit 9ce9efc82e
59 files changed
+29355 -2

No files matched your search

+595
View File
@@ -0,0 +1,595 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Usage Analytics — Nexus One AI</title>
<link rel="stylesheet" href="style.css?v=4">
<style>
/* ── Toolbar ── */
.an-toolbar { display:flex; align-items:center; gap:10px; margin-bottom:24px; flex-wrap:wrap; }
.an-period-btn {
padding:7px 14px; border-radius:8px; font-size:13px; font-weight:600; cursor:pointer;
border:1.5px solid var(--bdr); background:var(--navy2); color:var(--med); font-family:inherit; transition:.15s;
}
.an-period-btn.active { border-color:var(--purple); color:var(--purple); background:rgba(124,58,237,.1); }
.an-scope-badge { margin-left:auto; font-size:12px; padding:4px 12px; border-radius:20px; font-weight:600; }
.an-scope-badge.admin { background:rgba(34,197,94,.12); color:#15803D; }
.an-scope-badge.personal { background:rgba(124,58,237,.1); color:var(--purple); }
.an-export-btn {
padding:7px 14px; border-radius:8px; font-size:13px; font-weight:600; cursor:pointer;
border:1.5px solid var(--bdr); background:var(--navy2); color:var(--med); font-family:inherit; transition:.15s;
display:flex; align-items:center; gap:6px;
}
.an-export-btn:hover { border-color:var(--purple); color:var(--purple); }
/* ── Stat tiles ── */
.an-tiles { display:grid; grid-template-columns:repeat(3,1fr); gap:16px; margin-bottom:24px; }
@media(max-width:900px){ .an-tiles { grid-template-columns:repeat(2,1fr); } }
@media(max-width:540px){ .an-tiles { grid-template-columns:1fr; } }
.an-tile {
background:var(--navy2); border:1px solid var(--bdr); border-radius:14px; padding:20px 22px;
display:flex; flex-direction:column; gap:4px;
}
.an-tile-icon { font-size:20px; margin-bottom:4px; }
.an-tile-val { font-size:32px; font-weight:800; color:var(--ink); line-height:1; }
.an-tile-lbl { font-size:12px; color:var(--lt); }
.an-tile-delta { font-size:11px; font-weight:600; margin-top:2px; }
.an-tile-delta.up { color:#15803D; }
.an-tile-delta.down { color:#DC2626; }
.an-tile-delta.flat { color:var(--lt); }
/* ── Cards ── */
.an-card { background:var(--navy2); border:1px solid var(--bdr); border-radius:14px; padding:22px 24px; margin-bottom:20px; }
.an-card-head { display:flex; align-items:center; gap:10px; margin-bottom:16px; }
.an-card-title { font-size:15px; font-weight:700; color:var(--ink); flex:1; }
.an-card-sub { font-size:12px; color:var(--lt); }
/* ── Charts ── */
.an-chart-wrap canvas { max-width:100%; display:block; }
/* ── Tables ── */
.an-table-wrap { border:1px solid var(--bdr); border-radius:10px; overflow:hidden; }
table.an-table { width:100%; border-collapse:collapse; }
.an-table th { background:var(--bg); padding:10px 14px; text-align:left; font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.5px; color:var(--lt); border-bottom:1px solid var(--bdr); }
.an-table td { padding:10px 14px; border-bottom:1px solid var(--bdr); font-size:13px; color:var(--ink); }
.an-table tr:last-child td { border-bottom:none; }
.an-table tr:hover td { background:var(--bg); }
.an-bar-bg { background:var(--bg); border-radius:4px; height:5px; margin-top:5px; }
.an-bar-fill { background:var(--purple); height:5px; border-radius:4px; transition:width .4s; }
.an-empty { text-align:center; color:var(--lt); padding:32px 0; font-size:13px; }
/* ── Grid layouts ── */
.an-grid-2 { display:grid; grid-template-columns:1fr 1fr; gap:20px; }
.an-grid-3 { display:grid; grid-template-columns:1fr 1fr 1fr; gap:20px; }
@media(max-width:900px){ .an-grid-2,.an-grid-3 { grid-template-columns:1fr; } }
/* ── Feature usage pills ── */
.an-feat-row { display:flex; align-items:center; gap:10px; margin-bottom:12px; }
.an-feat-row:last-child { margin-bottom:0; }
.an-feat-icon { font-size:16px; width:28px; flex-shrink:0; }
.an-feat-name { font-size:13px; font-weight:600; color:var(--ink); width:130px; flex-shrink:0; }
.an-feat-bar-wrap { flex:1; background:var(--bg); border-radius:4px; height:8px; }
.an-feat-bar { height:8px; border-radius:4px; transition:width .4s; }
.an-feat-count { font-size:12px; color:var(--lt); width:50px; text-align:right; flex-shrink:0; }
/* ── Peak hours ── */
.an-heatmap { display:grid; grid-template-columns:repeat(24,1fr); gap:3px; margin-top:8px; }
.an-heat-cell {
aspect-ratio:1; border-radius:3px; background:var(--bg);
cursor:default; position:relative;
}
.an-heat-cell:hover::after {
content:attr(data-tip); position:absolute; bottom:calc(100% + 4px); left:50%; transform:translateX(-50%);
background:var(--ink); color:white; font-size:10px; padding:3px 7px; border-radius:5px; white-space:nowrap; z-index:10;
}
.an-hour-labels { display:grid; grid-template-columns:repeat(24,1fr); gap:3px; margin-top:4px; }
.an-hour-label { font-size:9px; color:var(--lt); text-align:center; }
/* ── Token budget ── */
.an-budget-wrap { display:flex; align-items:center; gap:16px; }
.an-budget-bar-outer { flex:1; background:var(--bg); border-radius:8px; height:12px; }
.an-budget-bar-inner { height:12px; border-radius:8px; transition:width .5s; background:var(--purple); }
.an-budget-bar-inner.warn { background:#D97706; }
.an-budget-bar-inner.danger { background:#DC2626; }
.an-budget-pct { font-size:14px; font-weight:700; color:var(--ink); width:44px; text-align:right; flex-shrink:0; }
.an-budget-lbl { font-size:12px; color:var(--lt); margin-top:6px; }
/* ── Notice ── */
.an-notice { background:rgba(124,58,237,.08); border:1px solid rgba(124,58,237,.25); border-radius:10px; padding:12px 16px; font-size:13px; color:var(--purple); margin-bottom:20px; }
</style>
</head>
<body>
<header class="topnav">
<a href="index.html" class="brand">Nexus One <span>AI</span></a>
<nav>
<a href="index.html">Home</a>
<a href="quickstart.html">Quick Start</a>
<a href="prompts.html">Prompt Library</a>
<a href="usecases.html">Use Cases</a>
<span class="nav-sep"></span>
<div class="nav-dropdown">
<button class="nav-drop-btn">Help ▾</button>
<div class="nav-drop-menu">
<span class="nav-drop-cat">LEARN /</span>
<a href="quickstart.html">Quick Start</a>
<a href="models.html">Models</a>
<span class="nav-drop-cat">SUPPORT /</span>
<a href="troubleshooting.html">Troubleshoot</a>
<a href="faq.html">FAQ</a>
<span class="nav-drop-cat">MORE /</span>
<a href="glossary.html">Glossary</a>
<a href="whats-new.html">What's New</a>
</div>
</div>
<div class="nav-dropdown">
<button class="nav-drop-btn active">Admin ▾</button>
<div class="nav-drop-menu nav-drop-menu-wide">
<span class="nav-drop-cat">DOCS /</span>
<a href="security.html">Security & Privacy</a>
<a href="admin.html">Admin Guide</a>
<span class="nav-drop-cat">MONITOR /</span>
<a href="dashboard.html">Dashboard</a>
<a href="analytics.html" class="active">Usage Analytics</a>
<a href="audit.html">Audit Log</a>
<a href="feedback.html">Feedback &amp; Ratings</a>
<span class="nav-drop-cat">MANAGE /</span>
<a href="users.html">Users</a>
<a href="teams.html">Teams</a>
<a href="models-admin.html">Model Manager</a>
<a href="training.html">Training</a>
<a href="knowledge.html">Knowledge Base</a>
<span class="nav-drop-cat">TOOLS /</span>
<a href="apikeys.html">API Keys</a>
<a href="benchmark.html">Benchmarking</a>
<a href="model-compare.html">Model Compare</a>
<a href="api-playground.html">API Playground</a>
<a href="guardrails.html">Guardrails</a>
<a href="rag-quality.html">RAG Quality</a>
<a href="router.html">Model Router</a>
<a href="connectors.html">Connectors</a>
<span class="nav-drop-cat">SYSTEM /</span>
<a href="console.html">Console</a>
<a href="settings.html">Settings</a>
</div>
</div>
<div class="nav-dropdown">
<button class="nav-drop-btn">AI Tools ▾</button>
<div class="nav-drop-menu">
<span class="nav-drop-cat">INTELLIGENCE /</span>
<a href="documents.html">Document Intelligence</a>
<a href="chat-multi.html">Multimodal Chat</a>
<a href="prompt-studio.html">Prompt Studio</a>
<a href="meeting.html">Meeting Assistant</a>
<span class="nav-drop-cat">AUTOMATION /</span>
<a href="agents.html">Agent Builder</a>
<a href="schedules.html">Scheduled Jobs</a>
<a href="workflows.html">Workflow Automation</a>
<span class="nav-drop-cat">QUALITY /</span>
<a href="evals.html">AI Eval Suite</a>
<a href="chatrooms.html">Chat Rooms</a>
</div>
</div>
</nav>
<a href="notifications.html" style="position:relative">🔔</a>
<span class="badge" data-brand="tier">Basic Tier</span>
<div id="nav-org-logo" class="nav-org-logo"></div>
</header>
<div class="page-hero">
<div class="label">Admin · Analytics</div>
<h1>Usage Analytics</h1>
<p>Track AI usage across users, models, features, and time — all on-premises, all private.</p>
</div>
<div class="content">
<!-- Toolbar -->
<div class="an-toolbar">
<button class="an-period-btn active" onclick="setPeriod(7,this)">Last 7 days</button>
<button class="an-period-btn" onclick="setPeriod(30,this)">Last 30 days</button>
<button class="an-period-btn" onclick="setPeriod(90,this)">Last 90 days</button>
<button class="an-export-btn" onclick="exportCsv()">⬇️ Export CSV</button>
<span class="an-scope-badge admin" id="scope-badge">🏢 Org-wide view</span>
</div>
<div class="an-notice" id="an-notice" style="display:none"></div>
<!-- 6 Stat tiles -->
<div class="an-tiles">
<div class="an-tile">
<div class="an-tile-icon">🔢</div>
<div class="an-tile-val" id="stat-queries">—</div>
<div class="an-tile-lbl">Total Queries</div>
<div class="an-tile-delta up" id="delta-queries"></div>
</div>
<div class="an-tile">
<div class="an-tile-icon">🪙</div>
<div class="an-tile-val" id="stat-tokens">—</div>
<div class="an-tile-lbl">Tokens Processed</div>
<div class="an-tile-delta up" id="delta-tokens"></div>
</div>
<div class="an-tile">
<div class="an-tile-icon">⚡</div>
<div class="an-tile-val" id="stat-avg">—</div>
<div class="an-tile-lbl">Avg Response Time</div>
<div class="an-tile-delta flat" id="delta-avg"></div>
</div>
<div class="an-tile">
<div class="an-tile-icon">👥</div>
<div class="an-tile-val" id="stat-users">—</div>
<div class="an-tile-lbl">Active Users</div>
<div class="an-tile-delta up" id="delta-users"></div>
</div>
<div class="an-tile">
<div class="an-tile-icon">📄</div>
<div class="an-tile-val" id="stat-docs">—</div>
<div class="an-tile-lbl">Documents Processed</div>
<div class="an-tile-delta up" id="delta-docs"></div>
</div>
<div class="an-tile">
<div class="an-tile-icon">🤖</div>
<div class="an-tile-val" id="stat-agents">—</div>
<div class="an-tile-lbl">Agent Runs</div>
<div class="an-tile-delta up" id="delta-agents"></div>
</div>
</div>
<!-- Daily queries chart -->
<div class="an-card">
<div class="an-card-head">
<div class="an-card-title">Queries per Day</div>
<div class="an-card-sub" id="chart-subtitle"></div>
</div>
<div class="an-chart-wrap">
<canvas id="chart-daily" height="100"></canvas>
</div>
</div>
<!-- Feature usage + Token budget -->
<div class="an-grid-2" style="margin-bottom:20px">
<div class="an-card" style="margin-bottom:0">
<div class="an-card-head">
<div class="an-card-title">Usage by Feature</div>
<div class="an-card-sub">queries routed per tool</div>
</div>
<div id="feature-usage"></div>
</div>
<div class="an-card" style="margin-bottom:0">
<div class="an-card-head">
<div class="an-card-title">Token Budget</div>
<div class="an-card-sub" id="budget-period-lbl">this period</div>
</div>
<div id="token-budget"></div>
</div>
</div>
<!-- Peak hours -->
<div class="an-card">
<div class="an-card-head">
<div class="an-card-title">Peak Activity Hours</div>
<div class="an-card-sub">average queries per hour of day (local time)</div>
</div>
<div class="an-heatmap" id="heatmap"></div>
<div class="an-hour-labels" id="hour-labels"></div>
</div>
<!-- Models + Top users -->
<div class="an-grid-2">
<div class="an-card" style="margin-bottom:0">
<div class="an-card-head">
<div class="an-card-title">Model Usage</div>
</div>
<div id="models-table"></div>
</div>
<div class="an-card" id="users-card" style="margin-bottom:0">
<div class="an-card-head">
<div class="an-card-title">Top Users</div>
<div class="an-card-sub">admin view</div>
</div>
<div id="users-table"></div>
</div>
</div>
</div>
<footer>
<p>Nexus One AI &nbsp;·&nbsp; Powered by Cezen &nbsp;·&nbsp; <span data-brand="tier">Basic Tier</span></p>
<p>Questions? <a href="mailto:support@cezentech.com">support@cezentech.com</a> &nbsp;·&nbsp; <a href="https://cezentech.com" target="_blank">cezentech.com</a></p>
</footer>
<script>
const _API = '/api';
let currentDays = 7;
let currentData = null;
/* ── Mock data generator ─────────────────────────────────────────────────── */
function mockData(days) {
const seed = days * 137;
const rng = (min, max, s=1) => min + Math.abs(Math.sin(seed * s) * (max - min)) | 0;
const queries = rng(420, 980, 1) * (days / 7);
const tokens = queries * rng(280, 640, 2);
const daily = [];
const today = new Date();
for (let i = days - 1; i >= 0; i--) {
const d = new Date(today); d.setDate(d.getDate() - i);
const key = d.toISOString().slice(0,10);
const weekday = d.getDay();
const base = weekday === 0 || weekday === 6 ? rng(8,30,i+3) : rng(40,120,i+3);
daily.push({ day: key, queries: base });
}
return {
is_admin: true,
totals: {
total_queries: Math.round(queries),
total_tokens: Math.round(tokens),
avg_duration_ms: rng(820, 2400, 5),
active_users: rng(8, 24, 6),
docs_processed: rng(60, 340, 7),
agent_runs: rng(15, 80, 8),
},
deltas: {
queries: '+12%', tokens: '+18%', avg: '−4%', users: '+3', docs: '+28%', agents: '+9%'
},
daily,
models: [
{ model: 'llama3:70b', queries: rng(180, 420, 10) },
{ model: 'mistral:7b', queries: rng(90, 200, 11) },
{ model: 'llava:13b', queries: rng(40, 120, 12) },
{ model: 'codellama:34b', queries: rng(20, 80, 13) },
{ model: 'mixtral:8x7b', queries: rng(10, 50, 14) },
],
top_users: [
{ username: 'rajesh.k', queries: rng(80, 180, 20), dept: 'Finance' },
{ username: 'priya.m', queries: rng(60, 130, 21), dept: 'Legal' },
{ username: 'arun.s', queries: rng(40, 100, 22), dept: 'HR' },
{ username: 'deepa.v', queries: rng(30, 80, 23), dept: 'IT' },
{ username: 'kiran.p', queries: rng(20, 60, 24), dept: 'Operations' },
],
features: [
{ name: 'Multimodal Chat', icon: '💬', queries: rng(150, 400, 30), color: '#7C3AED' },
{ name: 'Document Intelligence', icon: '📄', queries: rng(80, 200, 31), color: '#EC4899' },
{ name: 'Agent Builder', icon: '🤖', queries: rng(40, 100, 32), color: '#0EA5E9' },
{ name: 'Workflows', icon: '⚙️', queries: rng(20, 80, 33), color: '#10B981' },
{ name: 'Meeting Assistant', icon: '🎙️', queries: rng(15, 60, 34), color: '#F59E0B' },
{ name: 'Knowledge Base', icon: '🧠', queries: rng(10, 40, 35), color: '#6366F1' },
],
peak_hours: Array.from({length:24}, (_,h) => {
const isWork = h >= 9 && h <= 18;
const isLunch = h === 13;
return isLunch ? rng(5,15,h+40) : isWork ? rng(20,80,h+40) : rng(0,8,h+40);
}),
token_budget: {
used: Math.round(tokens),
total: 50_000_000,
label: `${(tokens/1_000_000).toFixed(1)}M / 50M tokens`
}
};
}
/* ── Period ──────────────────────────────────────────────────────────────── */
function setPeriod(days, btn) {
currentDays = days;
document.querySelectorAll('.an-period-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
loadAnalytics();
}
/* ── Load ────────────────────────────────────────────────────────────────── */
async function loadAnalytics() {
try {
const res = await fetch(`${_API}/analytics/summary?days=${currentDays}`, {credentials:'include'});
if (!res.ok) throw new Error();
currentData = await res.json();
} catch(e) {
currentData = mockData(currentDays);
}
render(currentData);
}
/* ── Render ──────────────────────────────────────────────────────────────── */
function fmt(n) {
if (n >= 1_000_000) return (n/1_000_000).toFixed(1) + 'M';
if (n >= 1_000) return (n/1_000).toFixed(1) + 'K';
return Math.round(n).toLocaleString();
}
function render(d) {
// Scope badge
const badge = document.getElementById('scope-badge');
const notice = document.getElementById('an-notice');
if (d.is_admin) {
badge.textContent = '🏢 Org-wide view'; badge.className = 'an-scope-badge admin';
notice.style.display = 'none';
} else {
badge.textContent = '👤 My usage'; badge.className = 'an-scope-badge personal';
notice.textContent = 'Showing your personal usage only. Admins see organisation-wide data.';
notice.style.display = '';
}
// Tiles
const t = d.totals;
const deltas = d.deltas || {};
setTile('queries', fmt(t.total_queries), deltas.queries, 'up');
setTile('tokens', fmt(t.total_tokens), deltas.tokens, 'up');
const avgMs = t.avg_duration_ms;
setTile('avg', avgMs >= 1000 ? (avgMs/1000).toFixed(1)+'s' : Math.round(avgMs)+'ms', deltas.avg, 'flat');
setTile('users', String(t.active_users || '—'), deltas.users, 'up');
setTile('docs', fmt(t.docs_processed || 0), deltas.docs, 'up');
setTile('agents', fmt(t.agent_runs || 0), deltas.agents, 'up');
// Charts
document.getElementById('chart-subtitle').textContent = `${currentDays}-day total: ${fmt(d.totals.total_queries)} queries`;
renderDailyChart(d.daily);
renderFeatureUsage(d.features || []);
renderTokenBudget(d.token_budget || {used: t.total_tokens, total: 50_000_000});
renderHeatmap(d.peak_hours || []);
renderBarTable('models-table', d.models, 'model', 'queries', 'Model', 'Queries');
if (d.is_admin && d.top_users) {
document.getElementById('users-card').style.display = '';
renderBarTable('users-table', d.top_users, 'username', 'queries', 'User', 'Queries', r => r.dept ? `<span style="font-size:11px;color:var(--lt)">${escHtml(r.dept)}</span>` : '');
} else {
document.getElementById('users-card').style.display = 'none';
}
}
function setTile(key, val, delta, dir) {
document.getElementById('stat-'+key).textContent = val;
const el = document.getElementById('delta-'+key);
if (el && delta) {
el.textContent = delta + ' vs prev period';
el.className = 'an-tile-delta ' + (delta.startsWith('+') ? 'up' : delta.startsWith('−') || delta.startsWith('-') ? 'down' : 'flat');
}
}
/* ── Daily chart ─────────────────────────────────────────────────────────── */
function renderDailyChart(daily) {
const canvas = document.getElementById('chart-daily');
const ctx = canvas.getContext('2d');
const labels = [], data = [];
const byDay = {};
(daily || []).forEach(r => { byDay[r.day] = r.queries; });
const today = new Date();
for (let i = currentDays - 1; i >= 0; i--) {
const d = new Date(today); d.setDate(d.getDate() - i);
const key = d.toISOString().slice(0,10);
labels.push(key.slice(5));
data.push(byDay[key] || 0);
}
const W = canvas.parentElement.clientWidth;
canvas.width = W; canvas.height = 100;
const H = canvas.height;
const padL=36, padR=12, padT=12, padB=28;
const cW=W-padL-padR, cH=H-padT-padB;
const maxVal = Math.max(...data, 1);
ctx.clearRect(0,0,W,H);
// grid
ctx.strokeStyle='rgba(0,0,0,.06)'; ctx.lineWidth=1;
for (let i=0;i<=4;i++) {
const y=padT+cH-(i/4)*cH;
ctx.beginPath(); ctx.moveTo(padL,y); ctx.lineTo(W-padR,y); ctx.stroke();
ctx.fillStyle='#9CA3AF'; ctx.font='10px system-ui'; ctx.textAlign='right';
ctx.fillText(fmt(Math.round((maxVal*i)/4)), padL-4, y+3);
}
if (!data.length) return;
const step = cW/(data.length-1||1);
// gradient fill
const grad=ctx.createLinearGradient(0,padT,0,padT+cH);
grad.addColorStop(0,'rgba(124,58,237,.22)'); grad.addColorStop(1,'rgba(124,58,237,.01)');
ctx.beginPath();
data.forEach((v,i)=>{ const x=padL+i*step, y=padT+cH-(v/maxVal)*cH; i===0?ctx.moveTo(x,y):ctx.lineTo(x,y); });
ctx.lineTo(padL+(data.length-1)*step,padT+cH); ctx.lineTo(padL,padT+cH); ctx.closePath();
ctx.fillStyle=grad; ctx.fill();
// line
ctx.beginPath(); ctx.strokeStyle='#7C3AED'; ctx.lineWidth=2; ctx.lineJoin='round';
data.forEach((v,i)=>{ const x=padL+i*step, y=padT+cH-(v/maxVal)*cH; i===0?ctx.moveTo(x,y):ctx.lineTo(x,y); });
ctx.stroke();
// dots
data.forEach((v,i)=>{ const x=padL+i*step, y=padT+cH-(v/maxVal)*cH; ctx.beginPath(); ctx.arc(x,y,3,0,Math.PI*2); ctx.fillStyle='#7C3AED'; ctx.fill(); });
// x-labels
const every=Math.ceil(data.length/8);
ctx.fillStyle='#9CA3AF'; ctx.font='10px system-ui'; ctx.textAlign='center';
labels.forEach((lbl,i)=>{ if(i%every===0||i===labels.length-1) ctx.fillText(lbl,padL+i*step,H-6); });
}
/* ── Feature usage ───────────────────────────────────────────────────────── */
function renderFeatureUsage(features) {
const el = document.getElementById('feature-usage');
if (!features.length) { el.innerHTML = '<div class="an-empty">No data</div>'; return; }
const max = Math.max(...features.map(f=>f.queries), 1);
el.innerHTML = features.map(f => {
const pct = Math.round((f.queries/max)*100);
return `<div class="an-feat-row">
<span class="an-feat-icon">${f.icon||'🔹'}</span>
<span class="an-feat-name">${escHtml(f.name)}</span>
<div class="an-feat-bar-wrap"><div class="an-feat-bar" style="width:${pct}%;background:${f.color||'var(--purple)'}"></div></div>
<span class="an-feat-count">${fmt(f.queries)}</span>
</div>`;
}).join('');
}
/* ── Token budget ────────────────────────────────────────────────────────── */
function renderTokenBudget(budget) {
const el = document.getElementById('token-budget');
document.getElementById('budget-period-lbl').textContent = `last ${currentDays} days`;
const pct = Math.min(100, Math.round((budget.used/budget.total)*100));
const cls = pct >= 90 ? 'danger' : pct >= 70 ? 'warn' : '';
el.innerHTML = `
<div class="an-budget-wrap">
<div class="an-budget-bar-outer"><div class="an-budget-bar-inner ${cls}" style="width:${pct}%"></div></div>
<div class="an-budget-pct">${pct}%</div>
</div>
<div class="an-budget-lbl">${fmt(budget.used)} of ${fmt(budget.total)} tokens used</div>
<div style="margin-top:16px;display:flex;flex-direction:column;gap:6px">
<div style="display:flex;justify-content:space-between;font-size:12px;color:var(--lt)">
<span>Avg tokens per query</span><span style="color:var(--ink);font-weight:600">${currentData?.totals?.total_queries ? fmt(Math.round(budget.used/currentData.totals.total_queries)) : '—'}</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:12px;color:var(--lt)">
<span>Estimated monthly</span><span style="color:var(--ink);font-weight:600">${fmt(Math.round(budget.used*(30/currentDays)))}</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:12px;color:var(--lt)">
<span>Budget remaining</span><span style="color:${cls==='danger'?'#DC2626':cls==='warn'?'#D97706':'#15803D'};font-weight:600">${fmt(budget.total-budget.used)}</span>
</div>
</div>`;
}
/* ── Peak hours heatmap ──────────────────────────────────────────────────── */
function renderHeatmap(hourData) {
const hm = document.getElementById('heatmap');
const hl = document.getElementById('hour-labels');
const max = Math.max(...hourData, 1);
hm.innerHTML = hourData.map((v,h) => {
const pct = v/max;
const alpha = (0.08 + pct*0.85).toFixed(2);
const tip = `${h}:00 — ${v} avg queries`;
return `<div class="an-heat-cell" style="background:rgba(124,58,237,${alpha})" data-tip="${tip}"></div>`;
}).join('');
hl.innerHTML = hourData.map((_,h) => `<div class="an-hour-label">${h%3===0?h+'h':''}</div>`).join('');
}
/* ── Bar table ───────────────────────────────────────────────────────────── */
function renderBarTable(containerId, rows, nameKey, valKey, nameHeader, valHeader, extraFn) {
const el = document.getElementById(containerId);
if (!rows || !rows.length) { el.innerHTML = '<div class="an-empty">No data yet</div>'; return; }
const max = Math.max(...rows.map(r=>r[valKey]), 1);
let html = `<div class="an-table-wrap"><table class="an-table">
<tr><th>${nameHeader}</th><th style="text-align:right">${valHeader}</th></tr>`;
rows.forEach(r => {
const pct = Math.round((r[valKey]/max)*100);
const extra = extraFn ? extraFn(r) : '';
html += `<tr><td>
<div style="display:flex;align-items:center;gap:6px"><span style="font-weight:600">${escHtml(String(r[nameKey]||'—'))}</span>${extra}</div>
<div class="an-bar-bg"><div class="an-bar-fill" style="width:${pct}%"></div></div>
</td><td style="text-align:right;color:var(--lt);font-variant-numeric:tabular-nums">${fmt(r[valKey])}</td></tr>`;
});
el.innerHTML = html + '</table></div>';
}
/* ── Export CSV ──────────────────────────────────────────────────────────── */
function exportCsv() {
if (!currentData) return;
const rows = [['Date','Queries']];
(currentData.daily||[]).forEach(r => rows.push([r.day, r.queries]));
const csv = rows.map(r=>r.join(',')).join('\n');
const a = document.createElement('a');
a.href = 'data:text/csv;charset=utf-8,' + encodeURIComponent(csv);
a.download = `cezen-analytics-${currentDays}d.csv`;
a.click();
}
function escHtml(s) { return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
loadAnalytics();
window.addEventListener('resize', () => { if(currentData) renderDailyChart(currentData.daily); });
</script>
<script src="auth.js"></script>
<script src="branding.js"></script>
</body>
</html>