aipackage/cezen-portal/notifications.html
Jino Jose b3eec34158 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.
2026-07-09 11:11:38 +05:30

475 lines
27 KiB
HTML

<!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=12">
<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 {
width: 20px;
height: 20px;
color: currentColor;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.nt-filter-icon svg { width: 16px; height: 16px; }
.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 {
width: 40px;
height: 40px;
margin: 0 auto 12px;
opacity: .35;
color: var(--text-secondary);
}
.nt-empty-icon svg { width: 100%; height: 100%; }
.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;
color: currentColor; flex-shrink: 0;
}
.nt-item-icon svg { width: 18px; height: 18px; }
.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">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>
<a href="teams.html">Teams</a>
<a href="models-admin.html">Model Catalog</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">MCP & Enterprise 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">AI Workspace</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 Studio</a>
<a href="schedules.html">Scheduled Jobs</a>
<a href="workflows.html">Workflow Designer</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="nav-icon-btn active" style="position:relative" title="Notifications">
<span class="icon-svg icon-md" aria-hidden="true">
<svg viewBox="0 0 24 24"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
</span>
<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 icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M4 4h16v16H4z"/><path d="m4 7 8 6 8-6"/></svg></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 icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="5"/></svg></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 icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg></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 icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><circle cx="12" cy="13" r="8"/><path d="M12 9v4l2 2"/><path d="M9 2h6"/><path d="M12 2v3"/></svg></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 icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><rect x="7" y="8" width="10" height="8" rx="2"/><path d="M12 4v4M9 16v2M15 16v2M6 10H4M20 10h-2"/><circle cx="10" cy="12" r="1"/><circle cx="14" cy="12" r="1"/></svg></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 icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg></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 icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h.01A1.65 1.65 0 0 0 10.5 3.1V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51h.01a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v.01a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></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 icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg></span> Critical
</button>
<button class="nt-filter-btn" onclick="setFilter('sev-error', this)">
<span class="nt-filter-icon icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="m15 9-6 6M9 9l6 6"/></svg></span> Error
</button>
<button class="nt-filter-btn" onclick="setFilter('sev-warning', this)">
<span class="nt-filter-icon icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M12 9v4"/><path d="M12 17h.01"/><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/></svg></span> Warning
</button>
<button class="nt-filter-btn" onclick="setFilter('sev-info', this)">
<span class="nt-filter-icon icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 12v4"/><path d="M12 8h.01"/></svg></span> Info
</button>
</div>
<!-- Main -->
<div class="nt-main">
<div class="nt-header">
<div class="nt-title icon-label"><span class="icon-svg icon-lg" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg></span>Notifications</div>
<div class="nt-header-actions">
<button class="nt-btn" onclick="markAllRead()"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="m20 6-11 11-5-5"/></svg></span>Mark all read</span></button>
<button class="nt-btn danger" onclick="clearAll()"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/></svg></span>Clear all</span></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 icon-svg"><svg viewBox="0 0 24 24"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg></div><p>Loading…</p></div>
</div>
</div>
</div>
<script>
const SOURCE_ICONS = {
guardrail:'<span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg></span>',
scheduler:'<span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><circle cx="12" cy="13" r="8"/><path d="M12 9v4l2 2"/><path d="M9 2h6"/><path d="M12 2v3"/></svg></span>',
agent:'<span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><rect x="7" y="8" width="10" height="8" rx="2"/><path d="M12 4v4M9 16v2M15 16v2M6 10H4M20 10h-2"/><circle cx="10" cy="12" r="1"/><circle cx="14" cy="12" r="1"/></svg></span>',
rag:'<span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg></span>',
system:'<span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h.01A1.65 1.65 0 0 0 10.5 3.1V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51h.01a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v.01a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></span>'
};
const SEV_ICONS = {
info:'<span class="icon-svg icon-md" aria-hidden="true"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 12v4"/><path d="M12 8h.01"/></svg></span>',
warning:'<span class="icon-svg icon-md" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M12 9v4"/><path d="M12 17h.01"/><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/></svg></span>',
error:'<span class="icon-svg icon-md" aria-hidden="true"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="m15 9-6 6M9 9l6 6"/></svg></span>',
critical:'<span class="icon-svg icon-md" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg></span>'
};
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 icon-svg"><svg viewBox="0 0 24 24"><path d="m20 6-11 11-5-5"/></svg></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"><span class="icon-label">${SOURCE_ICONS[n.source]||''}<span>${n.source}</span></span></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"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="m20 6-11 11-5-5"/></svg></span></button>
<button class="nt-icon-btn del" onclick="event.stopPropagation();deleteNotif(${n.id})" title="Delete"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M6 6l12 12M18 6 6 18"/></svg></span></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?v=3"></script>
</body>
</html>