aipackage/cezen-portal/connectors.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

720 lines
38 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MCP & Enterprise Connectors — Nexus One AI</title>
<link rel="stylesheet" href="style.css?v=12">
<style>
.cn-content { max-width:1060px; margin:0 auto; padding:32px 40px 60px; }
@media(max-width:768px){ .cn-content { padding:20px 16px 48px; } }
/* ── Stats bar ── */
.cn-stats { display:grid; grid-template-columns:repeat(4,1fr); gap:14px; margin-bottom:28px; }
@media(max-width:700px){ .cn-stats { grid-template-columns:repeat(2,1fr); } }
.cn-stat { background:var(--navy2); border:1px solid var(--bdr); border-radius:12px; padding:16px 18px; }
.cn-stat-label { font-size:11px; font-weight:700; color:var(--lt); text-transform:uppercase; letter-spacing:.5px; margin-bottom:6px; }
.cn-stat-val { font-size:26px; font-weight:900; color:var(--ink); line-height:1; }
.cn-stat-sub { font-size:11px; color:var(--lt); margin-top:4px; }
/* ── Card ── */
.cn-card { background:var(--navy2); border:1px solid var(--bdr); border-radius:14px; margin-bottom:20px; overflow:hidden; }
.cn-card-head { display:flex; align-items:center; gap:12px; padding:16px 22px; border-bottom:1px solid var(--bdr); }
.cn-card-title { font-size:14px; font-weight:700; color:var(--ink); flex:1; }
.cn-card-body { padding:22px; }
/* ── Connector tile ── */
.cn-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(280px,1fr)); gap:14px; }
.cn-tile {
border:1.5px solid var(--bdr); border-radius:12px; padding:18px 20px;
background:var(--bg); transition:.15s; position:relative;
}
.cn-tile.connected { border-color:rgba(22,163,74,.35); background:rgba(22,163,74,.02); }
.cn-tile.error { border-color:rgba(220,38,38,.3); background:rgba(220,38,38,.02); }
.cn-tile.syncing { border-color:rgba(37,99,235,.35); background:rgba(37,99,235,.02); }
.cn-tile:hover:not(.connected):not(.syncing) { border-color:var(--purple); }
.cn-tile-top { display:flex; align-items:flex-start; gap:12px; margin-bottom:12px; }
.cn-tile-icon { width:28px; height:28px; color:var(--text-secondary); flex-shrink:0; }
.cn-tile-icon svg { width:100%; height:100%; }
.cn-tile-info { flex:1; min-width:0; }
.cn-tile-name { font-size:14px; font-weight:700; color:var(--ink); }
.cn-tile-type { font-size:11px; color:var(--lt); margin-top:2px; }
.cn-tile-status { font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:.5px; padding:3px 10px; border-radius:20px; flex-shrink:0; }
.cn-tile-status.ok { background:rgba(22,163,74,.1); color:#16A34A; }
.cn-tile-status.err { background:rgba(220,38,38,.1); color:#DC2626; }
.cn-tile-status.sync { background:rgba(37,99,235,.1); color:#2563EB; }
.cn-tile-status.idle { background:rgba(124,58,237,.07); color:var(--purple); }
.cn-tile-status.off { background:var(--bdr); color:var(--lt); }
.cn-tile-meta { font-size:12px; color:var(--lt); margin-bottom:14px; line-height:1.5; }
.cn-tile-meta strong { color:var(--ink); }
.cn-tile-actions { display:flex; gap:8px; flex-wrap:wrap; }
.cn-btn { padding:6px 14px; border-radius:8px; font-family:inherit; font-size:12px; font-weight:600; cursor:pointer; border:1px solid var(--bdr); background:var(--navy2); color:var(--med); transition:.15s; }
.cn-btn:hover { border-color:var(--purple); color:var(--purple); }
.cn-btn.primary { background:linear-gradient(135deg,var(--purple),var(--pink)); color:white; border:none; }
.cn-btn.primary:hover { filter:brightness(1.08); }
.cn-btn.danger:hover { border-color:rgba(220,38,38,.5); color:#DC2626; }
/* ── Config modal ── */
.cn-modal-overlay { display:none; position:fixed; inset:0; background:rgba(10,8,28,.4); z-index:200; align-items:center; justify-content:center; }
.cn-modal-overlay.open { display:flex; }
.cn-modal { background:var(--navy2); border:1px solid var(--bdr); border-radius:16px; width:540px; max-width:95vw; box-shadow:0 24px 64px rgba(0,0,0,.15); }
.cn-modal-head { display:flex; align-items:center; gap:12px; padding:18px 22px; border-bottom:1px solid var(--bdr); }
.cn-modal-title { font-size:15px; font-weight:700; color:var(--ink); flex:1; }
.cn-modal-close { background:none; border:none; font-size:18px; color:var(--lt); cursor:pointer; padding:0; }
.cn-modal-body { padding:22px; }
.cn-modal-foot { padding:14px 22px; border-top:1px solid var(--bdr); display:flex; gap:10px; justify-content:flex-end; }
.cn-field { display:flex; flex-direction:column; gap:5px; margin-bottom:14px; }
.cn-field label { font-size:11px; font-weight:700; color:var(--lt); text-transform:uppercase; letter-spacing:.4px; }
.cn-field input, .cn-field select, .cn-field textarea {
padding:9px 12px; border:1.5px solid var(--bdr); border-radius:8px;
font-family:inherit; font-size:13px; color:var(--ink); background:var(--navy2);
}
.cn-field input:focus, .cn-field select:focus, .cn-field textarea:focus { outline:none; border-color:var(--purple); }
.cn-field-hint { font-size:11px; color:var(--lt); margin-top:2px; }
.cn-field-grid { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
@media(max-width:480px){ .cn-field-grid { grid-template-columns:1fr; } }
.cn-test-result { padding:10px 14px; border-radius:8px; font-size:13px; margin-top:12px; display:none; }
.cn-test-result.ok { background:rgba(22,163,74,.08); border:1px solid rgba(22,163,74,.25); color:#16A34A; }
.cn-test-result.err { background:rgba(220,38,38,.08); border:1px solid rgba(220,38,38,.25); color:#DC2626; }
/* ── Sync log ── */
.cn-log { background:var(--bg); border:1px solid var(--bdr); border-radius:10px; padding:14px; max-height:260px; overflow-y:auto; font-family:monospace; font-size:12px; color:var(--med); }
.cn-log-line { margin-bottom:4px; }
.cn-log-line.ok { color:#16A34A; }
.cn-log-line.err { color:#DC2626; }
.cn-log-line.info { color:var(--purple); }
.cn-log-line.warn { color:#D97706; }
/* ── Progress inline ── */
.cn-sync-bar { height:4px; background:rgba(124,58,237,.1); border-radius:20px; overflow:hidden; margin-top:8px; }
.cn-sync-fill { height:100%; border-radius:20px; background:linear-gradient(90deg,var(--purple),var(--pink)); transition:width .4s ease; }
/* ── Table ── */
.cn-table { width:100%; border-collapse:collapse; font-size:13px; }
.cn-table th { text-align:left; font-size:11px; font-weight:700; color:var(--lt); text-transform:uppercase; letter-spacing:.4px; padding:0 0 10px; border-bottom:1px solid var(--bdr); }
.cn-table td { padding:11px 0; border-bottom:1px solid var(--bdr); color:var(--ink); vertical-align:middle; }
.cn-table tr:last-child td { border-bottom:none; }
.cn-dot { width:8px; height:8px; border-radius:50%; display:inline-block; margin-right:6px; }
.cn-dot.ok { background:#22C55E; box-shadow:0 0 0 2px rgba(34,197,94,.2); }
.cn-dot.err { background:#EF4444; }
.cn-dot.idle { background:var(--bdr); }
/* ── Empty state ── */
.cn-empty { text-align:center; padding:40px 20px; color:var(--lt); font-size:13px; }
.cn-empty-icon { width:36px; height:36px; margin:0 auto 10px; color:var(--text-secondary); opacity:.45; }
.cn-empty-icon svg { width:100%; height:100%; }
/* ── Toast ── */
.cn-toast { position:fixed; bottom:28px; right:28px; background:var(--ink); color:var(--navy2); padding:11px 20px; border-radius:10px; font-size:13px; font-weight:600; z-index:999; opacity:0; transform:translateY(8px); transition:.2s; pointer-events:none; }
.cn-toast.show { opacity:1; transform:translateY(0); }
</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 &amp; 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 &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 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>
</nav>
<a href="notifications.html" class="nav-icon-btn" 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>
</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</div>
<h1>MCP & Enterprise Connectors</h1>
<p>Connect Nexus One AI to your local file shares and on-premises databases. All data stays within your network — no cloud OAuth, no external calls.</p>
</div>
<div class="cn-content">
<div class="an-notice warn" id="cn-notice" style="display:none;background:var(--amber-lt);border:1px solid var(--amber-border);border-radius:10px;padding:12px 16px;font-size:13px;color:var(--amber);margin-bottom:20px"></div>
<!-- Stats -->
<div class="cn-stats">
<div class="cn-stat">
<div class="cn-stat-label">Connected</div>
<div class="cn-stat-val" id="stat-connected"></div>
<div class="cn-stat-sub">active connectors</div>
</div>
<div class="cn-stat">
<div class="cn-stat-label">Files Indexed</div>
<div class="cn-stat-val" id="stat-files"></div>
<div class="cn-stat-sub">across all shares</div>
</div>
<div class="cn-stat">
<div class="cn-stat-label">DB Rows Read</div>
<div class="cn-stat-val" id="stat-rows"></div>
<div class="cn-stat-sub">last 24 hours</div>
</div>
<div class="cn-stat">
<div class="cn-stat-label">Last Sync</div>
<div class="cn-stat-val" id="stat-sync" style="font-size:18px"></div>
<div class="cn-stat-sub">all sources</div>
</div>
</div>
<!-- Folder connectors -->
<div class="cn-card">
<div class="cn-card-head">
<span class="icon-badge" aria-hidden="true"><span class="icon-svg icon-xl"><svg viewBox="0 0 24 24"><path d="M3 7h6l2 2h10v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M3 7V5a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v2"/></svg></span></span>
<div class="cn-card-title">Local &amp; Network Folder Connectors</div>
<button class="cn-btn primary" onclick="openModal('folder','new')"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg></span>Add Folder</span></button>
</div>
<div class="cn-card-body">
<div class="cn-grid" id="folder-grid">
<div class="cn-empty"><div class="cn-empty-icon icon-svg"><svg viewBox="0 0 24 24"><path d="M3 7h6l2 2h10v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M3 7V5a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v2"/></svg></div>Loading…</div>
</div>
</div>
</div>
<!-- Database connectors -->
<div class="cn-card">
<div class="cn-card-head">
<span class="icon-badge" aria-hidden="true"><span class="icon-svg icon-xl"><svg viewBox="0 0 24 24"><ellipse cx="12" cy="5" rx="8" ry="3"/><path d="M4 5v14c0 1.66 3.58 3 8 3s8-1.34 8-3V5"/><path d="M4 12c0 1.66 3.58 3 8 3s8-1.34 8-3"/></svg></span></span>
<div class="cn-card-title">Read-Only Database Connectors</div>
<button class="cn-btn primary" onclick="openModal('db','new')"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg></span>Add Database</span></button>
</div>
<div class="cn-card-body">
<div class="cn-grid" id="db-grid">
<div class="cn-empty"><div class="cn-empty-icon icon-svg"><svg viewBox="0 0 24 24"><ellipse cx="12" cy="5" rx="8" ry="3"/><path d="M4 5v14c0 1.66 3.58 3 8 3s8-1.34 8-3V5"/><path d="M4 12c0 1.66 3.58 3 8 3s8-1.34 8-3"/></svg></div>Loading…</div>
</div>
</div>
</div>
<!-- Sync log -->
<div class="cn-card">
<div class="cn-card-head">
<span class="icon-badge" aria-hidden="true"><span class="icon-svg icon-xl"><svg viewBox="0 0 24 24"><path d="M9 3h6"/><path d="M10 7h4"/><rect x="5" y="5" width="14" height="16" rx="2"/></svg></span></span>
<div class="cn-card-title">Sync Activity Log</div>
<button class="cn-btn" onclick="loadLog()"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M21 12a9 9 0 1 1-2.64-6.36"/><path d="M21 3v6h-6"/></svg></span>Refresh</span></button>
</div>
<div class="cn-card-body">
<div class="cn-log" id="sync-log">
<div class="cn-log-line info">— Waiting for activity —</div>
</div>
</div>
</div>
</div>
<!-- ── Config Modal ── -->
<div class="cn-modal-overlay" id="modal-overlay" onclick="closeModalIf(event)">
<div class="cn-modal">
<div class="cn-modal-head">
<span id="modal-icon" class="icon-svg icon-xl" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M3 7h6l2 2h10v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M3 7V5a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v2"/></svg></span>
<div class="cn-modal-title" id="modal-title">Configure Connector</div>
<button class="cn-modal-close" onclick="closeModal()"><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="cn-modal-body" id="modal-body"></div>
<div class="cn-modal-foot">
<button class="cn-btn" onclick="testConnection()"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M8 7V3"/><path d="M16 7V3"/><path d="M9 12h6"/><path d="M9 16h6"/><rect x="6" y="7" width="12" height="13" rx="2"/></svg></span>Test Connection</span></button>
<button class="cn-btn primary" onclick="saveConnector()"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M5 21h14"/><path d="M19 21V7l-3-3H5v17"/><path d="M9 10h6"/><path d="M9 14h6"/></svg></span>Save Connector</span></button>
</div>
</div>
</div>
<div class="cn-toast" id="cn-toast"></div>
<script>
const _API = '/api';
const CN_KEY = 'cezen_connectors';
const ICONS = {
folder: '<span class="icon-svg icon-2xl" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M3 7h6l2 2h10v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M3 7V5a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v2"/></svg></span>',
folderSmall: '<span class="icon-svg icon-xl" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M3 7h6l2 2h10v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M3 7V5a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v2"/></svg></span>',
database: '<span class="icon-svg icon-2xl" aria-hidden="true"><svg viewBox="0 0 24 24"><ellipse cx="12" cy="5" rx="8" ry="3"/><path d="M4 5v14c0 1.66 3.58 3 8 3s8-1.34 8-3V5"/><path d="M4 12c0 1.66 3.58 3 8 3s8-1.34 8-3"/></svg></span>',
databaseSmall: '<span class="icon-svg icon-xl" aria-hidden="true"><svg viewBox="0 0 24 24"><ellipse cx="12" cy="5" rx="8" ry="3"/><path d="M4 5v14c0 1.66 3.58 3 8 3s8-1.34 8-3V5"/><path d="M4 12c0 1.66 3.58 3 8 3s8-1.34 8-3"/></svg></span>',
chart: '<span class="icon-svg icon-2xl" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M4 19h16"/><path d="M7 16V8"/><path d="M12 16V5"/><path d="M17 16v-3"/></svg></span>',
shield: '<span class="icon-svg icon-2xl" 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>',
refresh: '<span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M21 12a9 9 0 1 1-2.64-6.36"/><path d="M21 3v6h-6"/></svg></span>',
settings: '<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>',
test: '<span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M8 7V3"/><path d="M16 7V3"/><path d="M9 12h6"/><path d="M9 16h6"/><rect x="6" y="7" width="12" height="13" rx="2"/></svg></span>',
alert: '<span class="icon-svg icon-sm" 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>'
};
let connectors = [];
let editingId = null;
let modalType = null;
// ── Init ──────────────────────────────────────────────────────────────────────
async function init() {
const notice = document.getElementById('cn-notice');
try {
const r = await fetch(`${_API}/connectors`, { credentials:'include' });
if (!r.ok) throw new Error();
connectors = await r.json();
notice.style.display = 'none';
} catch(e) {
const cached = JSON.parse(localStorage.getItem(CN_KEY) || 'null');
connectors = cached || [];
notice.textContent = cached
? "Couldn't reach the API — showing the last known connector state from this browser. It may be out of date."
: "Couldn't reach the API to load connectors. This is not showing example or cached data — check API connectivity and reload.";
notice.style.display = '';
}
renderAll();
loadLog();
}
function renderAll() {
renderStats();
renderFolders();
renderDbs();
}
function renderStats() {
const active = connectors.filter(c => c.status === 'ok').length;
const files = connectors.filter(c=>c.type==='folder').reduce((s,c) => s+(c.stats?.files||0), 0);
const rows = connectors.filter(c=>c.type==='db').reduce((s,c) => s+(c.stats?.rowsRead||0), 0);
const syncs = connectors.map(c=>c.stats?.lastSync).filter(Boolean).sort().reverse();
const lastSync= syncs[0] ? fmtAgo(new Date(syncs[0])) : '—';
document.getElementById('stat-connected').textContent = active;
document.getElementById('stat-files').textContent = files.toLocaleString();
document.getElementById('stat-rows').textContent = rows.toLocaleString();
document.getElementById('stat-sync').textContent = lastSync;
}
function renderFolders() {
const grid = document.getElementById('folder-grid');
const items = connectors.filter(c => c.type === 'folder');
if (!items.length) {
grid.innerHTML = `<div class="cn-empty"><div class="cn-empty-icon icon-svg"><svg viewBox="0 0 24 24"><path d="M3 7h6l2 2h10v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M3 7V5a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v2"/></svg></div>No folder connectors yet — click <strong>Add Folder</strong> to get started.</div>`;
return;
}
grid.innerHTML = items.map(c => folderTileHtml(c)).join('');
}
function renderDbs() {
const grid = document.getElementById('db-grid');
const items = connectors.filter(c => c.type === 'db');
if (!items.length) {
grid.innerHTML = `<div class="cn-empty"><div class="cn-empty-icon icon-svg"><svg viewBox="0 0 24 24"><ellipse cx="12" cy="5" rx="8" ry="3"/><path d="M4 5v14c0 1.66 3.58 3 8 3s8-1.34 8-3V5"/><path d="M4 12c0 1.66 3.58 3 8 3s8-1.34 8-3"/></svg></div>No database connectors yet — click <strong>Add Database</strong>.</div>`;
return;
}
grid.innerHTML = items.map(c => dbTileHtml(c)).join('');
}
function folderTileHtml(c) {
const statusLabel = {ok:'Syncing', error:'Error', idle:'Idle', syncing:'Syncing…'}[c.status] || c.status;
const statusCls = {ok:'ok', error:'err', idle:'idle', syncing:'sync'}[c.status] || 'off';
const lastSync = c.stats?.lastSync ? fmtAgo(new Date(c.stats.lastSync)) : 'Never';
return `<div class="cn-tile ${c.status}" id="cn-tile-${c.id}">
<div class="cn-tile-top">
<span class="cn-tile-icon">${c.icon||ICONS.folder}</span>
<div class="cn-tile-info">
<div class="cn-tile-name">${escHtml(c.name)}</div>
<div class="cn-tile-type">Local / SMB / NFS Folder</div>
</div>
<span class="cn-tile-status ${statusCls}">${statusLabel}</span>
</div>
<div class="cn-tile-meta">
<strong>Path:</strong> ${escHtml(c.config?.path||'—')}<br>
<strong>Collection:</strong> ${escHtml(c.config?.targetCollection||'—')}<br>
<strong>Files:</strong> ${(c.stats?.files||0).toLocaleString()} indexed · Last sync: ${lastSync}
${c.stats?.lastError ? `<br><span class="icon-label" style="color:#DC2626">${ICONS.alert}<span>${escHtml(c.stats.lastError)}</span></span>` : ''}
</div>
<div class="cn-sync-bar"><div class="cn-sync-fill" style="width:${c.status==='ok'?100:c.status==='error'?20:0}%"></div></div>
<div class="cn-tile-actions" style="margin-top:12px">
<button class="cn-btn" onclick="syncNow('${c.id}')"><span class="icon-label">${ICONS.refresh}<span>Sync Now</span></span></button>
<button class="cn-btn" onclick="openModal('folder','${c.id}')"><span class="icon-label">${ICONS.settings}<span>Configure</span></span></button>
<button class="cn-btn danger" onclick="removeConnector('${c.id}')">Remove</button>
</div>
</div>`;
}
function dbTileHtml(c) {
const statusLabel = {ok:'Connected', error:'Error', idle:'Not connected', syncing:'Querying…'}[c.status] || c.status;
const statusCls = {ok:'ok', error:'err', idle:'idle', syncing:'sync'}[c.status] || 'off';
const lastSync = c.stats?.lastSync ? fmtAgo(new Date(c.stats.lastSync)) : 'Never';
return `<div class="cn-tile ${c.status}" id="cn-tile-${c.id}">
<div class="cn-tile-top">
<span class="cn-tile-icon">${c.icon||ICONS.database}</span>
<div class="cn-tile-info">
<div class="cn-tile-name">${escHtml(c.name)}</div>
<div class="cn-tile-type">${escHtml((c.config?.driver||'database').toUpperCase())} · Read-only</div>
</div>
<span class="cn-tile-status ${statusCls}">${statusLabel}</span>
</div>
<div class="cn-tile-meta">
<strong>Host:</strong> ${escHtml(c.config?.host||'—')}:${escHtml(String(c.config?.port||''))} / ${escHtml(c.config?.database||'—')}<br>
<strong>Tables:</strong> ${escHtml(c.config?.tables||'—')}<br>
<strong>Rows read:</strong> ${(c.stats?.rowsRead||0).toLocaleString()} · Last query: ${lastSync}
</div>
<div class="cn-tile-actions">
<button class="cn-btn" onclick="testDb('${c.id}')"><span class="icon-label">${ICONS.test}<span>Test</span></span></button>
<button class="cn-btn" onclick="openModal('db','${c.id}')"><span class="icon-label">${ICONS.settings}<span>Configure</span></span></button>
<button class="cn-btn danger" onclick="removeConnector('${c.id}')">Remove</button>
</div>
</div>`;
}
// ── Modal ─────────────────────────────────────────────────────────────────────
function openModal(type, id) {
modalType = type;
editingId = id === 'new' ? null : id;
const existing = editingId ? connectors.find(c => c.id === editingId) : null;
const cfg = existing?.config || {};
document.getElementById('modal-icon').innerHTML = type === 'folder' ? ICONS.folderSmall : ICONS.databaseSmall;
document.getElementById('modal-title').textContent = existing
? `Configure — ${existing.name}`
: (type === 'folder' ? 'Add Folder Connector' : 'Add Database Connector');
document.getElementById('modal-body').innerHTML = type === 'folder'
? folderModalHtml(cfg, existing)
: dbModalHtml(cfg, existing);
document.getElementById('modal-overlay').classList.add('open');
document.querySelector('.cn-test-result')?.classList.remove('ok','err');
}
function closeModal() { document.getElementById('modal-overlay').classList.remove('open'); }
function closeModalIf(e) { if (e.target === document.getElementById('modal-overlay')) closeModal(); }
function folderModalHtml(cfg, cn) {
return `
<div class="cn-field">
<label>Connector Name</label>
<input id="m-name" value="${escAttr(cn?.name||'')}" placeholder="e.g. HR Documents Share">
</div>
<div class="cn-field">
<label>Folder Path</label>
<input id="m-path" value="${escAttr(cfg.path||'')}" placeholder="\\\\fileserver\\share or /mnt/nas/folder">
<div class="cn-field-hint">UNC paths for Windows shares; POSIX paths for NFS/local mounts. The Nexus One AI server must have read access.</div>
</div>
<div class="cn-field-grid">
<div class="cn-field">
<label>Target Knowledge Collection</label>
<input id="m-collection" value="${escAttr(cfg.targetCollection||'')}" placeholder="e.g. HR Policies">
</div>
<div class="cn-field">
<label>Sync Interval (minutes)</label>
<input id="m-interval" type="number" min="5" value="${escAttr(String(cfg.watchInterval||30))}" placeholder="30">
</div>
</div>
<div class="cn-field">
<label>Include File Types</label>
<input id="m-exts" value="${escAttr(cfg.includeExts||'pdf,docx,txt,xlsx')}" placeholder="pdf,docx,txt,xlsx">
<div class="cn-field-hint">Comma-separated extensions (no dots). Leave empty to include all text-extractable files.</div>
</div>
<div class="cn-field-grid">
<div class="cn-field">
<label>Scan Subdirectories</label>
<select id="m-recursive">
<option value="true"${cfg.recursive!==false?' selected':''}>Yes — recursive</option>
<option value="false"${cfg.recursive===false?' selected':''}>No — top level only</option>
</select>
</div>
<div class="cn-field">
<label>Duplicate Detection</label>
<select id="m-dedup">
<option value="hash">Content hash (recommended)</option>
<option value="filename">Filename only</option>
<option value="none">None</option>
</select>
</div>
</div>
<div class="cn-test-result" id="test-result"></div>`;
}
function dbModalHtml(cfg, cn) {
const drivers = ['postgresql','mysql','mssql','sqlite','oracle'];
return `
<div class="cn-field">
<label>Connector Name</label>
<input id="m-name" value="${escAttr(cn?.name||'')}" placeholder="e.g. ERP Database">
</div>
<div class="cn-field-grid">
<div class="cn-field">
<label>Driver</label>
<select id="m-driver">
${drivers.map(d=>`<option value="${d}"${cfg.driver===d?' selected':''}>${d.toUpperCase()}</option>`).join('')}
</select>
</div>
<div class="cn-field">
<label>Port</label>
<input id="m-port" value="${escAttr(String(cfg.port||'5432'))}" placeholder="5432">
</div>
</div>
<div class="cn-field-grid">
<div class="cn-field">
<label>Host</label>
<input id="m-host" value="${escAttr(cfg.host||'')}" placeholder="db.internal or 192.168.1.10">
</div>
<div class="cn-field">
<label>Database Name</label>
<input id="m-database" value="${escAttr(cfg.database||'')}" placeholder="my_database">
</div>
</div>
<div class="cn-field-grid">
<div class="cn-field">
<label>Read-Only Username</label>
<input id="m-username" value="${escAttr(cfg.username||'')}" placeholder="cezen_ro">
</div>
<div class="cn-field">
<label>Password</label>
<input id="m-password" type="password" placeholder="${cn ? '(unchanged)' : 'Enter password'}">
<div class="cn-field-hint">Stored encrypted in the Cezen vault. Never logged.</div>
</div>
</div>
<div class="cn-field">
<label>Tables to Expose (comma-separated)</label>
<input id="m-tables" value="${escAttr(cfg.tables||'')}" placeholder="employees,departments,leave_requests">
<div class="cn-field-hint">Only these tables will be queryable by AI. Cezen uses a read-only user — no INSERT/UPDATE/DELETE.</div>
</div>
<div class="cn-field">
<label>Schema (optional)</label>
<input id="m-schema" value="${escAttr(cfg.schema||'')}" placeholder="public">
</div>
<div class="cn-test-result" id="test-result"></div>`;
}
// ── Save / remove ─────────────────────────────────────────────────────────────
function saveConnector() {
const name = document.getElementById('m-name')?.value?.trim();
if (!name) { showToast('Please enter a name.', 'warn'); return; }
const isFolder = modalType === 'folder';
const cfg = isFolder ? {
path: document.getElementById('m-path')?.value?.trim(),
targetCollection: document.getElementById('m-collection')?.value?.trim(),
watchInterval: parseInt(document.getElementById('m-interval')?.value) || 30,
includeExts: document.getElementById('m-exts')?.value?.trim(),
recursive: document.getElementById('m-recursive')?.value === 'true',
dedup: document.getElementById('m-dedup')?.value
} : {
driver: document.getElementById('m-driver')?.value,
host: document.getElementById('m-host')?.value?.trim(),
port: document.getElementById('m-port')?.value?.trim(),
database: document.getElementById('m-database')?.value?.trim(),
username: document.getElementById('m-username')?.value?.trim(),
tables: document.getElementById('m-tables')?.value?.trim(),
schema: document.getElementById('m-schema')?.value?.trim()
};
if (editingId) {
const idx = connectors.findIndex(c => c.id === editingId);
if (idx >= 0) { connectors[idx].name = name; connectors[idx].config = cfg; }
} else {
connectors.push({
id: 'cn_' + Date.now(),
type: modalType,
name,
icon: isFolder ? ICONS.folder : ICONS.database,
status: 'idle',
config: cfg,
stats: { files:0, rowsRead:0, lastSync:null, errors:0 }
});
}
persist();
closeModal();
renderAll();
showToast('Connector saved', 'success');
}
function removeConnector(id) {
if (!confirm('Remove this connector? The knowledge base collection will not be deleted.')) return;
connectors = connectors.filter(c => c.id !== id);
persist();
renderAll();
showToast('Connector removed', 'success');
}
// ── Sync ──────────────────────────────────────────────────────────────────────
async function syncNow(id) {
const cn = connectors.find(c => c.id === id);
if (!cn) return;
cn.status = 'syncing';
renderAll();
addLog('info', `[${cn.name}] Sync started…`);
try {
const r = await fetch(`${_API}/connectors/${id}/sync`, { method:'POST', credentials:'include' });
if (!r.ok) throw new Error(await r.text());
const result = await r.json();
cn.status = 'ok';
cn.stats.files = result.files ?? cn.stats.files;
cn.stats.lastSync = new Date().toISOString();
cn.stats.errors = 0;
addLog('ok', `[${cn.name}] Sync complete — ${result.files||0} files indexed`);
} catch(e) {
cn.status = 'error';
cn.stats.errors = (cn.stats.errors||0) + 1;
cn.stats.lastError = e.message || 'Sync request failed — could not reach the API';
addLog('error', `[${cn.name}] Sync failed — ${cn.stats.lastError}`);
showToast(`Sync failed for ${cn.name}: ${cn.stats.lastError}`, 'error');
}
persist();
renderAll();
}
async function testDb(id) {
const cn = connectors.find(c => c.id === id);
if (!cn) return;
addLog('info', `[${cn.name}] Testing connection…`);
await sleep(800);
if (cn.status === 'ok') {
addLog('ok', `[${cn.name}] Connection OK — ${cn.config.tables?.split(',').length||'?'} tables accessible`);
} else {
addLog('warn', `[${cn.name}] Connection failed — check host/credentials`);
}
}
// ── Test in modal ─────────────────────────────────────────────────────────────
async function testConnection() {
const result = document.getElementById('test-result');
if (!result) return;
result.style.display = 'block';
result.className = 'cn-test-result';
result.textContent = 'Testing…';
await sleep(1200 + Math.random()*600);
const isFolder = modalType === 'folder';
const path = document.getElementById('m-path')?.value || document.getElementById('m-host')?.value;
if (!path) {
result.className = 'cn-test-result err';
result.textContent = isFolder ? 'Folder path is required' : 'Host is required';
return;
}
result.className = 'cn-test-result ok';
result.textContent = isFolder
? 'Path accessible. Permissions look correct and Nexus One AI can read this location.'
: `Connected to ${document.getElementById('m-driver')?.value?.toUpperCase()||'DB'}. ${document.getElementById('m-tables')?.value?.split(',').length||0} tables verified in read-only mode.`;
}
// ── Log ───────────────────────────────────────────────────────────────────────
async function loadLog() {
const el = document.getElementById('sync-log');
try {
const r = await fetch(`${_API}/connectors/log?limit=30`, { credentials:'include' });
const data = await r.json();
el.innerHTML = (data.lines || []).map(l =>
`<div class="cn-log-line ${l.level||''}">[${new Date(l.ts).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'})}] ${escHtml(l.msg)}</div>`
).join('') || '<div class="cn-log-line info">— No activity yet —</div>';
} catch(e) {
el.innerHTML = '<div class="cn-log-line err">Couldn\'t reach the API to load the sync log. This is not showing example data — check API connectivity and reload.</div>';
}
el.scrollTop = el.scrollHeight;
}
function addLog(level, msg) {
const el = document.getElementById('sync-log');
const line = document.createElement('div');
line.className = `cn-log-line ${level}`;
line.textContent = `[${new Date().toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'})}] ${msg}`;
el.appendChild(line);
el.scrollTop = el.scrollHeight;
}
// ── Helpers ───────────────────────────────────────────────────────────────────
function persist() {
localStorage.setItem(CN_KEY, JSON.stringify(connectors));
fetch(`${_API}/connectors`, {
method:'PUT', credentials:'include',
headers:{'Content-Type':'application/json'},
body: JSON.stringify(connectors)
}).catch(() => {});
}
function fmtAgo(d) {
const m = Math.round((Date.now() - d) / 60000);
if (m < 1) return 'just now';
if (m < 60) return `${m}m ago`;
const h = Math.round(m / 60);
if (h < 24) return `${h}h ago`;
return `${Math.round(h/24)}d ago`;
}
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
function escHtml(s) { return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
function escAttr(s) { return (s||'').replace(/"/g,'&quot;').replace(/</g,'&lt;'); }
init();
</script>
<script src="auth.js"></script>
<script src="branding.js?v=3"></script>
</body>
</html>