aipackage/cezen-portal/analytics.html
2026-06-30 10:51:41 +05:30

596 lines
28 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>