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

443 lines
21 KiB
HTML
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>Notifications — Nexus One AI</title>
<link rel="stylesheet" href="style.css?v=4">
<style>
.nt-layout { display: grid; grid-template-columns: 220px 1fr; min-height: calc(100vh - 64px); }
@media(max-width:700px){ .nt-layout { grid-template-columns: 1fr; } .nt-sidebar { display:none; } }
/* Sidebar */
.nt-sidebar {
border-right: 1px solid var(--bdr); background: var(--navy2);
padding: 20px 14px; display: flex; flex-direction: column; gap: 4px;
}
.nt-sidebar-title { font-size: 11px; font-weight: 700; color: var(--lt); text-transform: uppercase; letter-spacing: .5px; padding: 0 8px 10px; }
.nt-filter-btn {
display: flex; align-items: center; gap: 10px; padding: 9px 12px;
border-radius: 9px; cursor: pointer; border: none; background: none;
font-family: inherit; font-size: 13px; color: var(--med); text-align: left;
transition: .12s; width: 100%;
}
.nt-filter-btn:hover { background: rgba(124,58,237,.07); color: var(--ink); }
.nt-filter-btn.active { background: rgba(124,58,237,.12); color: var(--purple); font-weight: 700; }
.nt-filter-icon { font-size: 15px; width: 20px; text-align: center; }
.nt-filter-count {
margin-left: auto; background: var(--purple); color: white;
font-size: 10px; font-weight: 800; padding: 2px 7px;
border-radius: 20px; min-width: 20px; text-align: center;
}
.nt-filter-count.hidden { display: none; }
/* Main */
.nt-main { padding: 28px 32px; max-width: 860px; }
.nt-header { display: flex; align-items: center; gap: 14px; margin-bottom: 22px; }
.nt-title { font-size: 22px; font-weight: 800; color: var(--ink); flex: 1; }
.nt-header-actions { display: flex; gap: 8px; }
.nt-btn {
padding: 8px 16px; border-radius: 9px; font-family: inherit;
font-size: 13px; font-weight: 600; cursor: pointer; transition: .15s;
border: 1.5px solid var(--bdr); background: none; color: var(--med);
}
.nt-btn:hover { border-color: var(--purple); color: var(--purple); }
.nt-btn.primary { background: var(--purple); color: white; border-color: var(--purple); }
.nt-btn.primary:hover { background: #6d28d9; }
.nt-btn.danger:hover { border-color: #ef4444; color: #ef4444; }
/* List */
.nt-list { display: flex; flex-direction: column; gap: 8px; }
.nt-empty { text-align: center; padding: 60px 20px; color: var(--lt); }
.nt-empty-icon { font-size: 40px; margin-bottom: 12px; opacity: .35; }
.nt-item {
display: flex; gap: 14px; align-items: flex-start;
background: var(--navy2); border: 1.5px solid var(--bdr);
border-radius: 12px; padding: 14px 16px; cursor: pointer;
transition: .15s; position: relative;
}
.nt-item:hover { border-color: var(--purple); }
.nt-item.unread { border-left: 3px solid var(--purple); }
.nt-item.unread.sev-error { border-left-color: #ef4444; }
.nt-item.unread.sev-critical { border-left-color: #dc2626; }
.nt-item.unread.sev-warning { border-left-color: #f59e0b; }
.nt-item-icon {
width: 38px; height: 38px; border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 18px; flex-shrink: 0;
}
.nt-item-icon.info { background: rgba(124,58,237,.12); }
.nt-item-icon.warning { background: rgba(245,158,11,.12); }
.nt-item-icon.error { background: rgba(239,68,68,.12); }
.nt-item-icon.critical { background: rgba(220,38,38,.15); }
.nt-item-body { flex: 1; min-width: 0; }
.nt-item-header { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; flex-wrap: wrap; }
.nt-item-title { font-size: 14px; font-weight: 700; color: var(--ink); }
.nt-sev-badge {
font-size: 10px; font-weight: 800; text-transform: uppercase;
padding: 2px 8px; border-radius: 20px; letter-spacing: .3px;
}
.nt-sev-badge.info { background: rgba(124,58,237,.1); color: var(--purple); }
.nt-sev-badge.warning { background: rgba(245,158,11,.12); color: #d97706; }
.nt-sev-badge.error { background: rgba(239,68,68,.1); color: #dc2626; }
.nt-sev-badge.critical { background: rgba(220,38,38,.15); color: #b91c1c; }
.nt-src-badge {
font-size: 10px; font-weight: 700; padding: 2px 8px;
border-radius: 20px; background: rgba(100,116,139,.1); color: var(--med);
}
.nt-item-time { margin-left: auto; font-size: 11px; color: var(--lt); white-space: nowrap; }
.nt-item-body-text { font-size: 13px; color: var(--med); line-height: 1.55; }
.nt-item-link { font-size: 12px; color: var(--purple); margin-top: 5px; display: inline-block; }
.nt-item-actions { display: flex; flex-direction: column; gap: 4px; flex-shrink: 0; }
.nt-icon-btn {
background: none; border: none; cursor: pointer; padding: 4px 6px;
border-radius: 6px; color: var(--lt); font-size: 13px; transition: .12s;
}
.nt-icon-btn:hover { background: rgba(124,58,237,.1); color: var(--purple); }
.nt-icon-btn.del:hover { background: rgba(239,68,68,.1); color: #ef4444; }
.nt-unread-dot {
position: absolute; top: 14px; right: 14px;
width: 8px; height: 8px; border-radius: 50%; background: var(--purple);
}
.nt-unread-dot.hidden { display: none; }
/* Stats row */
.nt-stats { display: flex; gap: 12px; margin-bottom: 20px; flex-wrap: wrap; }
.nt-stat-card {
background: var(--navy2); border: 1.5px solid var(--bdr);
border-radius: 11px; padding: 12px 18px; min-width: 110px;
flex: 1;
}
.nt-stat-label { font-size: 11px; font-weight: 700; color: var(--lt); text-transform: uppercase; letter-spacing: .4px; margin-bottom: 4px; }
.nt-stat-val { font-size: 24px; font-weight: 800; color: var(--ink); }
.nt-stat-val.purple { color: var(--purple); }
.nt-stat-val.orange { color: #d97706; }
.nt-stat-val.red { color: #dc2626; }
</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">Usage Analytics</a>
<a href="audit.html">Audit Log</a>
<a href="feedback.html">Feedback & 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>
<a href="notifications.html" class="active" style="position:relative">
🔔 <span id="nav-badge" class="nt-filter-count hidden" style="position:absolute;top:-4px;right:-8px;font-size:9px;padding:1px 5px"></span>
</a>
</nav>
<span class="badge" data-brand="tier">Basic Tier</span>
<div id="nav-org-logo" class="nav-org-logo"></div>
</header>
<div class="nt-layout">
<!-- Sidebar -->
<div class="nt-sidebar">
<div class="nt-sidebar-title">Filter</div>
<button class="nt-filter-btn active" onclick="setFilter('all', this)">
<span class="nt-filter-icon">📬</span> All
<span class="nt-filter-count hidden" id="cnt-all"></span>
</button>
<button class="nt-filter-btn" onclick="setFilter('unread', this)">
<span class="nt-filter-icon">🔵</span> Unread
<span class="nt-filter-count hidden" id="cnt-unread"></span>
</button>
<div style="margin: 14px 8px 6px; font-size:11px;font-weight:700;color:var(--lt);text-transform:uppercase;letter-spacing:.4px">By Source</div>
<button class="nt-filter-btn" onclick="setFilter('guardrail', this)">
<span class="nt-filter-icon">🛡</span> Guardrails
<span class="nt-filter-count hidden" id="cnt-guardrail"></span>
</button>
<button class="nt-filter-btn" onclick="setFilter('scheduler', this)">
<span class="nt-filter-icon"></span> Scheduler
<span class="nt-filter-count hidden" id="cnt-scheduler"></span>
</button>
<button class="nt-filter-btn" onclick="setFilter('agent', this)">
<span class="nt-filter-icon">🤖</span> Agents
<span class="nt-filter-count hidden" id="cnt-agent"></span>
</button>
<button class="nt-filter-btn" onclick="setFilter('rag', this)">
<span class="nt-filter-icon">📚</span> RAG / KB
<span class="nt-filter-count hidden" id="cnt-rag"></span>
</button>
<button class="nt-filter-btn" onclick="setFilter('system', this)">
<span class="nt-filter-icon">⚙️</span> System
<span class="nt-filter-count hidden" id="cnt-system"></span>
</button>
<div style="margin: 14px 8px 6px; font-size:11px;font-weight:700;color:var(--lt);text-transform:uppercase;letter-spacing:.4px">By Severity</div>
<button class="nt-filter-btn" onclick="setFilter('sev-critical', this)">
<span class="nt-filter-icon">🔴</span> Critical
</button>
<button class="nt-filter-btn" onclick="setFilter('sev-error', this)">
<span class="nt-filter-icon">🟠</span> Error
</button>
<button class="nt-filter-btn" onclick="setFilter('sev-warning', this)">
<span class="nt-filter-icon">🟡</span> Warning
</button>
<button class="nt-filter-btn" onclick="setFilter('sev-info', this)">
<span class="nt-filter-icon">🔵</span> Info
</button>
</div>
<!-- Main -->
<div class="nt-main">
<div class="nt-header">
<div class="nt-title">🔔 Notifications</div>
<div class="nt-header-actions">
<button class="nt-btn" onclick="markAllRead()">✓ Mark all read</button>
<button class="nt-btn danger" onclick="clearAll()">🗑 Clear all</button>
</div>
</div>
<!-- Stats -->
<div class="nt-stats">
<div class="nt-stat-card">
<div class="nt-stat-label">Total</div>
<div class="nt-stat-val purple" id="stat-total"></div>
</div>
<div class="nt-stat-card">
<div class="nt-stat-label">Unread</div>
<div class="nt-stat-val purple" id="stat-unread"></div>
</div>
<div class="nt-stat-card">
<div class="nt-stat-label">Errors</div>
<div class="nt-stat-val red" id="stat-errors"></div>
</div>
<div class="nt-stat-card">
<div class="nt-stat-label">Warnings</div>
<div class="nt-stat-val orange" id="stat-warnings"></div>
</div>
</div>
<div class="nt-list" id="nt-list">
<div class="nt-empty"><div class="nt-empty-icon">🔔</div><p>Loading…</p></div>
</div>
</div>
</div>
<script>
const SOURCE_ICONS = { guardrail:'🛡', scheduler:'⏱', agent:'🤖', rag:'📚', system:'⚙️' };
const SEV_ICONS = { info:'', warning:'⚠️', error:'❌', critical:'🚨' };
let allNotifications = [];
let currentFilter = 'all';
const MOCK = [
{ id:1, title:'Guardrail triggered — PII detected', body:'User "ravi.k" attempted to upload a file containing Aadhaar numbers. The upload was blocked by the PII guardrail rule.', source:'guardrail', severity:'error', link:'guardrails.html', is_read:0, created_at: new Date(Date.now()-3*60000).toISOString() },
{ id:2, title:'Scheduled job failed — Nightly Audit Report', body:'Job "Nightly Audit Report" (ID 7) failed at 02:00 with error: Connection timeout to PostgreSQL connector. Last successful run was 2 days ago.', source:'scheduler', severity:'error', link:'schedules.html', is_read:0, created_at: new Date(Date.now()-2*60*60000).toISOString() },
{ id:3, title:'Agent run error — Contract Monitor', body:'Agent "Contract Expiry Monitor" encountered an error on run #42: Unable to parse PDF — file appears to be scanned without OCR layer.', source:'agent', severity:'warning', link:'agents.html', is_read:0, created_at: new Date(Date.now()-5*60*60000).toISOString() },
{ id:4, title:'RAG ingestion failed — Q3 Policy Docs', body:'Ingestion job for collection "Q3 Policy Docs" failed on 3 files. Reason: Unsupported file type (.pptx). Re-upload as PDF to proceed.', source:'rag', severity:'warning', link:'knowledge.html', is_read:0, created_at: new Date(Date.now()-8*60*60000).toISOString() },
{ id:5, title:'New user registered — awaiting approval', body:'User "priya.nair@iimb.ac.in" has registered and is pending admin approval.', source:'system', severity:'info', link:'users.html', is_read:1, created_at: new Date(Date.now()-1*24*60*60000).toISOString() },
{ id:6, title:'Model "llama3.1:70b" download complete', body:'Model llama3.1:70b (40.2 GB) has finished downloading and is ready for use in the Model Manager.', source:'system', severity:'info', link:'models-admin.html', is_read:1, created_at: new Date(Date.now()-2*24*60*60000).toISOString() },
{ id:7, title:'Guardrail triggered — prompt injection attempt', body:'A potential prompt injection was detected in chat session from user "external01". The message was flagged and not sent to the model.', source:'guardrail', severity:'critical', link:'guardrails.html', is_read:1, created_at: new Date(Date.now()-3*24*60*60000).toISOString() },
{ id:8, title:'RAG quality score dropped below threshold', body:'Collection "Legal Documents" average retrieval score dropped to 0.42 (threshold: 0.60). Consider re-indexing or adding more documents.', source:'rag', severity:'warning', link:'rag-quality.html', is_read:1, created_at: new Date(Date.now()-4*24*60*60000).toISOString() },
];
async function loadNotifications() {
try {
const r = await fetch('/api/notifications?limit=100');
if (!r.ok) throw new Error();
const d = await r.json();
allNotifications = d.notifications;
updateStats(d.unread_count);
} catch {
allNotifications = MOCK;
updateStats(MOCK.filter(n => !n.is_read).length);
}
render();
}
function updateStats(unreadCount) {
document.getElementById('stat-total').textContent = allNotifications.length;
document.getElementById('stat-unread').textContent = unreadCount;
document.getElementById('stat-errors').textContent = allNotifications.filter(n => n.severity === 'error' || n.severity === 'critical').length;
document.getElementById('stat-warnings').textContent = allNotifications.filter(n => n.severity === 'warning').length;
// Sidebar counts
document.getElementById('cnt-all').textContent = allNotifications.length;
document.getElementById('cnt-all').classList.toggle('hidden', !allNotifications.length);
document.getElementById('cnt-unread').textContent = unreadCount;
document.getElementById('cnt-unread').classList.toggle('hidden', !unreadCount);
for (const src of ['guardrail','scheduler','agent','rag','system']) {
const n = allNotifications.filter(x => x.source === src).length;
document.getElementById(`cnt-${src}`).textContent = n;
document.getElementById(`cnt-${src}`).classList.toggle('hidden', !n);
}
// Nav badge
const badge = document.getElementById('nav-badge');
if (unreadCount > 0) {
badge.textContent = unreadCount > 99 ? '99+' : unreadCount;
badge.classList.remove('hidden');
} else {
badge.classList.add('hidden');
}
}
function setFilter(f, btn) {
currentFilter = f;
document.querySelectorAll('.nt-filter-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
render();
}
function getFiltered() {
if (currentFilter === 'all') return allNotifications;
if (currentFilter === 'unread') return allNotifications.filter(n => !n.is_read);
if (currentFilter.startsWith('sev-')) return allNotifications.filter(n => n.severity === currentFilter.slice(4));
return allNotifications.filter(n => n.source === currentFilter);
}
function timeAgo(iso) {
const diff = (Date.now() - new Date(iso).getTime()) / 1000;
if (diff < 60) return `${Math.floor(diff)}s ago`;
if (diff < 3600) return `${Math.floor(diff/60)}m ago`;
if (diff < 86400) return `${Math.floor(diff/3600)}h ago`;
return `${Math.floor(diff/86400)}d ago`;
}
function render() {
const list = document.getElementById('nt-list');
const items = getFiltered();
if (!items.length) {
list.innerHTML = `<div class="nt-empty"><div class="nt-empty-icon">✅</div><p>No notifications here.</p></div>`;
return;
}
list.innerHTML = items.map(n => `
<div class="nt-item ${n.is_read ? '' : 'unread'} sev-${n.severity}" onclick="markRead(${n.id})" id="nt-${n.id}">
<div class="nt-item-icon ${n.severity}">${SEV_ICONS[n.severity] || '📌'}</div>
<div class="nt-item-body">
<div class="nt-item-header">
<span class="nt-item-title">${n.title}</span>
<span class="nt-sev-badge ${n.severity}">${n.severity}</span>
<span class="nt-src-badge">${SOURCE_ICONS[n.source]||''} ${n.source}</span>
<span class="nt-item-time">${timeAgo(n.created_at)}</span>
</div>
<div class="nt-item-body-text">${n.body}</div>
${n.link ? `<a class="nt-item-link" href="${n.link}" onclick="event.stopPropagation()">→ View details</a>` : ''}
</div>
<div class="nt-item-actions">
<button class="nt-icon-btn" onclick="event.stopPropagation();markRead(${n.id})" title="Mark read">✓</button>
<button class="nt-icon-btn del" onclick="event.stopPropagation();deleteNotif(${n.id})" title="Delete">✕</button>
</div>
<div class="nt-unread-dot ${n.is_read ? 'hidden' : ''}"></div>
</div>
`).join('');
}
async function markRead(id) {
const n = allNotifications.find(x => x.id === id);
if (!n || n.is_read) return;
n.is_read = 1;
try { await fetch(`/api/notifications/${id}/read`, { method:'PATCH' }); } catch {}
const unread = allNotifications.filter(x => !x.is_read).length;
updateStats(unread);
render();
}
async function markAllRead() {
allNotifications.forEach(n => n.is_read = 1);
try { await fetch('/api/notifications/read-all', { method:'POST' }); } catch {}
updateStats(0);
render();
}
async function deleteNotif(id) {
allNotifications = allNotifications.filter(x => x.id !== id);
try { await fetch(`/api/notifications/${id}`, { method:'DELETE' }); } catch {}
const unread = allNotifications.filter(x => !x.is_read).length;
updateStats(unread);
render();
}
async function clearAll() {
if (!confirm('Clear all notifications?')) return;
allNotifications = [];
try { await fetch('/api/notifications', { method:'DELETE' }); } catch {}
updateStats(0);
render();
}
loadNotifications();
// Poll every 30s for new notifications
setInterval(loadNotifications, 30000);
</script>
<script src="auth.js"></script>
<script src="branding.js"></script>
</body>
</html>