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

749 lines
35 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Connectors — Nexus One AI</title>
<link rel="stylesheet" href="style.css?v=4">
<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 { font-size:28px; flex-shrink:0; }
.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 { font-size:36px; margin-bottom:10px; }
/* ── 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">Audit Log</a>
<a href="feedback.html">Feedback &amp; Ratings</a>
<span class="nav-drop-cat">MANAGE /</span>
<a href="users.html">Users</a>
<a href="teams.html">Teams</a>
<a href="models-admin.html">Model Manager</a>
<a href="training.html">Training</a>
<a href="knowledge.html">Knowledge Base</a>
<span class="nav-drop-cat">TOOLS /</span>
<a href="apikeys.html">API Keys</a>
<a href="benchmark.html">Benchmarking</a>
<a href="model-compare.html">Model Compare</a>
<a href="api-playground.html">API Playground</a>
<a href="guardrails.html">Guardrails</a>
<a href="rag-quality.html">RAG Quality</a>
<a href="router.html">Model Router</a>
<a href="connectors.html">Connectors</a>
<span class="nav-drop-cat">SYSTEM /</span>
<a href="console.html">Console</a>
<a href="settings.html">Settings</a>
</div>
</div>
<div class="nav-dropdown">
<button class="nav-drop-btn">AI Tools ▾</button>
<div class="nav-drop-menu">
<span class="nav-drop-cat">INTELLIGENCE /</span>
<a href="documents.html">Document Intelligence</a>
<a href="chat-multi.html">Multimodal Chat</a>
<a href="prompt-studio.html">Prompt Studio</a>
<a href="meeting.html">Meeting Assistant</a>
<span class="nav-drop-cat">AUTOMATION /</span>
<a href="agents.html">Agent Builder</a>
<a href="schedules.html">Scheduled Jobs</a>
<a href="workflows.html">Workflow Automation</a>
<span class="nav-drop-cat">QUALITY /</span>
<a href="evals.html">AI Eval Suite</a>
<a href="chatrooms.html">Chat Rooms</a>
</div>
</div>
</nav>
<a href="notifications.html" style="position:relative">🔔</a>
<span class="badge" data-brand="tier">Basic Tier</span>
<div id="nav-org-logo" class="nav-org-logo"></div>
</header>
<div class="page-hero">
<div class="label">Admin</div>
<h1>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">
<!-- 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 style="font-size:20px">📁</span>
<div class="cn-card-title">Local &amp; Network Folder Connectors</div>
<button class="cn-btn primary" onclick="openModal('folder','new')"> Add Folder</button>
</div>
<div class="cn-card-body">
<div class="cn-grid" id="folder-grid">
<div class="cn-empty"><div class="cn-empty-icon">📂</div>Loading…</div>
</div>
</div>
</div>
<!-- Database connectors -->
<div class="cn-card">
<div class="cn-card-head">
<span style="font-size:20px">🗄️</span>
<div class="cn-card-title">Read-Only Database Connectors</div>
<button class="cn-btn primary" onclick="openModal('db','new')"> Add Database</button>
</div>
<div class="cn-card-body">
<div class="cn-grid" id="db-grid">
<div class="cn-empty"><div class="cn-empty-icon">🗄️</div>Loading…</div>
</div>
</div>
</div>
<!-- Sync log -->
<div class="cn-card">
<div class="cn-card-head">
<span style="font-size:20px">📋</span>
<div class="cn-card-title">Sync Activity Log</div>
<button class="cn-btn" onclick="loadLog()">↻ Refresh</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" style="font-size:22px">📁</span>
<div class="cn-modal-title" id="modal-title">Configure Connector</div>
<button class="cn-modal-close" onclick="closeModal()"></button>
</div>
<div class="cn-modal-body" id="modal-body"></div>
<div class="cn-modal-foot">
<button class="cn-btn" onclick="testConnection()">🔌 Test Connection</button>
<button class="cn-btn primary" onclick="saveConnector()">Save Connector</button>
</div>
</div>
</div>
<div class="cn-toast" id="cn-toast"></div>
<script>
const _API = '/api';
const CN_KEY = 'cezen_connectors';
let connectors = [];
let editingId = null;
let modalType = null;
// ── Mock data ─────────────────────────────────────────────────────────────────
const MOCK_CONNECTORS = [
{
id:'cn_1', type:'folder', name:'HR Documents Share',
icon:'📁', status:'ok',
config:{ path:'\\\\fileserver\\hr-docs', watchInterval:30, targetCollection:'HR Policies', includeExts:'pdf,docx,xlsx', recursive:true },
stats:{ files:142, lastSync: new Date(Date.now()-25*60000).toISOString(), errors:0 }
},
{
id:'cn_2', type:'folder', name:'Finance Reports',
icon:'📊', status:'ok',
config:{ path:'/mnt/nas/finance', watchInterval:60, targetCollection:'Finance Reports', includeExts:'xlsx,pdf,csv', recursive:false },
stats:{ files:67, lastSync: new Date(Date.now()-2*3600000).toISOString(), errors:0 }
},
{
id:'cn_3', type:'folder', name:'Legal Contracts',
icon:'⚖️', status:'error',
config:{ path:'\\\\lawserver\\contracts', watchInterval:15, targetCollection:'Legal Contracts', includeExts:'pdf,docx', recursive:true },
stats:{ files:89, lastSync: new Date(Date.now()-48*3600000).toISOString(), errors:3, lastError:'Mount point not reachable: \\\\lawserver\\contracts' }
},
{
id:'cn_db1', type:'db', name:'ERP Database (Read-only)',
icon:'🗄️', status:'ok',
config:{ driver:'postgresql', host:'erp-db.internal', port:'5432', database:'erp_prod', username:'cezen_ro', schema:'public', tables:'employees,departments,leave_requests' },
stats:{ rowsRead:12400, lastSync: new Date(Date.now()-10*60000).toISOString(), errors:0 }
},
{
id:'cn_db2', type:'db', name:'HRMS MySQL',
icon:'🗃️', status:'idle',
config:{ driver:'mysql', host:'hrms.internal', port:'3306', database:'hrms', username:'cezen_ro', schema:'', tables:'employees,payroll,attendance' },
stats:{ rowsRead:0, lastSync:null, errors:0 }
}
];
// ── Init ──────────────────────────────────────────────────────────────────────
async function init() {
try {
const r = await fetch(`${_API}/connectors`, { credentials:'include' });
if (!r.ok) throw new Error();
connectors = await r.json();
} catch(e) {
connectors = JSON.parse(localStorage.getItem(CN_KEY) || 'null') || MOCK_CONNECTORS;
}
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">📂</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">🗄️</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||'📁'}</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 style="color:#DC2626">⚠ ${escHtml(c.stats.lastError)}</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}')">↻ Sync Now</button>
<button class="cn-btn" onclick="openModal('folder','${c.id}')">⚙ Configure</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||'🗄️'}</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}')">🔌 Test</button>
<button class="cn-btn" onclick="openModal('db','${c.id}')">⚙ Configure</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').textContent = type === 'folder' ? '📁' : '🗄️';
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) { toast('Please enter a name'); 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 ? '📁' : '🗄️',
status: 'idle',
config: cfg,
stats: { files:0, rowsRead:0, lastSync:null, errors:0 }
});
}
persist();
closeModal();
renderAll();
toast('✓ Connector saved');
}
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();
toast('Connector removed');
}
// ── 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) {
// Mock success
await sleep(1200 + Math.random()*800);
const added = Math.floor(Math.random()*8) + 1;
cn.status = 'ok';
cn.stats.files = (cn.stats.files||0) + added;
cn.stats.lastSync = new Date().toISOString();
cn.stats.errors = 0;
addLog('ok', `[${cn.name}] Sync complete — ${added} new file${added>1?'s':''} indexed`);
}
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 ? 'No folder path specified' : 'No host specified'}`;
return;
}
result.className = 'cn-test-result ok';
result.textContent = isFolder
? `✓ Path accessible — permissions OK, 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 (read-only)`;
}
// ── 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) {
// Mock log
const mockLines = [
{ level:'info', ts: new Date(Date.now()-5*60000).toISOString(), msg:'[HR Documents Share] Sync started' },
{ level:'ok', ts: new Date(Date.now()-4*60000).toISOString(), msg:'[HR Documents Share] 3 new files indexed (leave_policy_2026.pdf, org_chart_q2.xlsx, onboarding_checklist.docx)' },
{ level:'ok', ts: new Date(Date.now()-4*60000).toISOString(), msg:'[HR Documents Share] Sync complete — 142 files total' },
{ level:'info', ts: new Date(Date.now()-62*60000).toISOString(), msg:'[Finance Reports] Sync started' },
{ level:'ok', ts: new Date(Date.now()-61*60000).toISOString(), msg:'[Finance Reports] No changes detected (67 files unchanged)' },
{ level:'err', ts: new Date(Date.now()-2*3600000).toISOString(), msg:'[Legal Contracts] Mount failed: cannot access \\\\lawserver\\contracts — Connection refused' },
{ level:'warn', ts: new Date(Date.now()-2*3600000).toISOString(), msg:'[Legal Contracts] Retrying in 15 minutes (attempt 3/5)' },
{ level:'info', ts: new Date(Date.now()-3*3600000).toISOString(), msg:'[ERP Database] Schema refresh — 3 tables, 12,400 rows indexed' },
{ level:'ok', ts: new Date(Date.now()-3*3600000).toISOString(), msg:'[ERP Database] Connection OK — PostgreSQL 15.2' }
];
el.innerHTML = mockLines.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('');
}
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 toast(msg) {
const el = document.getElementById('cn-toast');
el.textContent = msg; el.classList.add('show');
setTimeout(() => el.classList.remove('show'), 2200);
}
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"></script>
</body>
</html>