Sync cezen-portal/ from live working copy (V1.0 GA polish + search-icon ordering fix)
Brings the ISO-packaged portal copy current with the live cezen-portal/ working directory, which had drifted across the entire V1.0 GA polish initiative (Phases 1-10) plus this session's topnav icon-order bugfix. - 52 existing files updated, 2 new pages added (about.html, health.html) - cgit/cezen-portal/ is now byte-identical to cezen-portal/ - Backend files (rag_ingest.py, cezen_license.py) already in sync; main.py intentionally left as-is (cgit is ahead there with workstation-category commercial-fit logic not yet backported to the live working copy) Next: rebuild the ISO (bash autoinstall/build-iso.sh) to bake this in.
This commit is contained in:
1 parent
7540da04a8
commit
b3eec34158
57 files changed
+2832
-1354
No files matched your search
+23
-82
@@ -4,7 +4,7 @@
|
||||
<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">
|
||||
<link rel="stylesheet" href="style.css?v=12">
|
||||
<style>
|
||||
/* ── Toolbar ── */
|
||||
.an-toolbar { display:flex; align-items:center; gap:10px; margin-bottom:24px; flex-wrap:wrap; }
|
||||
@@ -97,6 +97,7 @@
|
||||
|
||||
/* ── 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; }
|
||||
.an-notice.warn { background:var(--amber-lt); border-color:var(--amber-border); color:var(--amber); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -133,6 +134,7 @@
|
||||
<a href="dashboard.html">Dashboard</a>
|
||||
<a href="analytics.html" class="active">Usage Analytics</a>
|
||||
<a href="audit.html">Governance & Audit</a>
|
||||
<a href="health.html">Health Center</a>
|
||||
<a href="feedback.html">Feedback & Ratings</a>
|
||||
<span class="nav-drop-cat">MANAGE /</span>
|
||||
<a href="users.html">Users</a>
|
||||
@@ -172,7 +174,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<a href="notifications.html" style="position:relative">🔔</a>
|
||||
<a href="notifications.html" class="nav-icon-btn" style="position:relative" aria-label="Notifications"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M15 17h5l-1.4-1.4A2 2 0 0 1 18 14.2V11a6 6 0 1 0-12 0v3.2a2 2 0 0 1-.6 1.4L4 17h5"/><path d="M10 17a2 2 0 0 0 4 0"/></svg></span></a>
|
||||
<span class="badge" data-brand="tier">Basic Tier</span>
|
||||
<div id="nav-org-logo" class="nav-org-logo"></div>
|
||||
</header>
|
||||
@@ -190,8 +192,8 @@
|
||||
<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>
|
||||
<button class="an-export-btn" onclick="exportCsv()"><span class="icon-svg icon-xs" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M7 10l5 5 5-5"/><path d="M12 15V3"/></svg></span> Export CSV</button>
|
||||
<span class="an-scope-badge admin" id="scope-badge"><span class="icon-svg icon-xs" aria-hidden="true"><svg viewBox="0 0 24 24"><rect x="4" y="2" width="16" height="20" rx="1"/><path d="M9 22v-4h6v4M9 6h1M9 10h1M9 14h1M14 6h1M14 10h1M14 14h1"/></svg></span> Org-wide view</span>
|
||||
</div>
|
||||
|
||||
<div class="an-notice" id="an-notice" style="display:none"></div>
|
||||
@@ -199,37 +201,37 @@
|
||||
<!-- 6 Stat tiles -->
|
||||
<div class="an-tiles">
|
||||
<div class="an-tile">
|
||||
<div class="an-tile-icon">🔢</div>
|
||||
<div class="an-tile-icon">Q</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-icon">T</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-icon">A</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-icon">U</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-icon">D</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-icon">R</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>
|
||||
@@ -307,72 +309,6 @@ 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;
|
||||
@@ -383,14 +319,19 @@ function setPeriod(days, btn) {
|
||||
|
||||
/* ── Load ────────────────────────────────────────────────────────────────── */
|
||||
async function loadAnalytics() {
|
||||
const notice = document.getElementById('an-notice');
|
||||
try {
|
||||
const res = await fetch(`${_API}/analytics/summary?days=${currentDays}`, {credentials:'include'});
|
||||
if (!res.ok) throw new Error();
|
||||
currentData = await res.json();
|
||||
notice.classList.remove('warn');
|
||||
render(currentData);
|
||||
} catch(e) {
|
||||
currentData = mockData(currentDays);
|
||||
notice.classList.add('warn');
|
||||
notice.textContent = "Couldn't reach the API to load usage analytics. The numbers below are not being shown — check API connectivity and reload.";
|
||||
notice.style.display = '';
|
||||
document.getElementById('feature-usage').innerHTML = '<div class="an-empty">Data unavailable</div>';
|
||||
}
|
||||
render(currentData);
|
||||
}
|
||||
|
||||
/* ── Render ──────────────────────────────────────────────────────────────── */
|
||||
@@ -405,10 +346,10 @@ function render(d) {
|
||||
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';
|
||||
badge.innerHTML = '<span class="icon-svg icon-xs" aria-hidden="true"><svg viewBox="0 0 24 24"><rect x="4" y="2" width="16" height="20" rx="1"/><path d="M9 22v-4h6v4M9 6h1M9 10h1M9 14h1M14 6h1M14 10h1M14 14h1"/></svg></span> Org-wide view'; badge.className = 'an-scope-badge admin';
|
||||
notice.style.display = 'none';
|
||||
} else {
|
||||
badge.textContent = '👤 My usage'; badge.className = 'an-scope-badge personal';
|
||||
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 = '';
|
||||
}
|
||||
@@ -506,7 +447,7 @@ function renderFeatureUsage(features) {
|
||||
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-icon">${f.icon||'<span class="icon-svg icon-xs" aria-hidden="true"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/></svg></span>'}</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>
|
||||
@@ -590,6 +531,6 @@ window.addEventListener('resize', () => { if(currentData) renderDailyChart(curre
|
||||
</script>
|
||||
|
||||
<script src="auth.js"></script>
|
||||
<script src="branding.js"></script>
|
||||
<script src="branding.js?v=3"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user