// ========================================== // Chat Switchboard – UI Admin // ========================================== // Fullscreen admin panel with category + section navigation. // All loadAdmin*() functions unchanged — same endpoints, same DOM IDs. // ── Category → Section mapping ──────────── const ADMIN_SECTIONS = { people: ['users', 'teams', 'groups'], ai: ['providers', 'models', 'personas', 'roles', 'knowledgeBases', 'memory'], routing: ['health', 'routing', 'capabilities'], system: ['settings', 'storage', 'extensions'], monitoring: ['usage', 'audit', 'stats'], }; const ADMIN_LABELS = { users: 'Users', teams: 'Teams', groups: 'Groups', providers: 'Providers', models: 'Models', personas: 'Personas', roles: 'Roles', knowledgeBases: 'Knowledge', memory: 'Memory', health: 'Health', routing: 'Routing', capabilities: 'Capabilities', settings: 'Settings', storage: 'Storage', extensions: 'Extensions', usage: 'Usage', audit: 'Audit', stats: 'Stats', }; // Section → loader mapping (reuses all existing loadAdmin* functions) const ADMIN_LOADERS = { users: () => UI.loadAdminUsers(), teams: () => UI.loadAdminTeams(), groups: () => UI.loadAdminGroups(), roles: () => UI.loadAdminRoles(), providers: () => UI.loadAdminProviders(), models: () => UI.loadAdminModels(), personas: () => UI.loadAdminPersonas(), knowledgeBases: () => { if (typeof KnowledgeUI === 'undefined') { console.error('[Admin] KnowledgeUI not loaded — check js/knowledge-ui.js'); return; } KnowledgeUI.openAdminPanel(); }, memory: () => { if (typeof MemoryUI !== 'undefined') { MemoryUI.openAdminPanel(); } else { console.error('[Admin] MemoryUI not loaded — check js/memory-ui.js'); } }, settings: () => UI.loadAdminSettings(), storage: () => typeof loadAdminStorage === 'function' ? loadAdminStorage() : null, extensions: () => UI.loadAdminExtensions(), health: () => UI.loadAdminHealth(), routing: () => UI.loadAdminRouting(), capabilities: () => UI.loadAdminCapabilities(), usage: () => UI.loadAdminUsage(), audit: () => UI.loadAuditLog(), stats: () => UI.loadAdminStats(), channels: () => UI.loadAdminChannels(), }; // Find which category a section belongs to function _adminCatForSection(section) { for (const [cat, sections] of Object.entries(ADMIN_SECTIONS)) { if (sections.includes(section)) return cat; } return 'people'; } Object.assign(UI, { // ── Admin Panel State ───────────────────── _adminCat: 'people', _adminSection: 'users', // ── Open / Close ────────────────────────── openAdmin(cat, section) { UI._adminCat = cat || 'people'; UI._adminSection = section || ADMIN_SECTIONS[UI._adminCat][0]; const panel = document.getElementById('adminPanel'); panel.style.display = 'flex'; // Show current user email const userEl = document.getElementById('adminCurrentUser'); if (userEl && API.user) userEl.textContent = API.user.email || API.user.username || ''; UI._renderAdminNav(); UI._switchAdminSection(UI._adminSection); }, closeAdmin() { document.getElementById('adminPanel').style.display = 'none'; }, // ── Navigate to specific section ────────── // Called from command palette, deep links, etc. openAdminSection(section) { const cat = _adminCatForSection(section); UI.openAdmin(cat, section); }, // ── Category switch ─────────────────────── switchAdminCategory(cat) { UI._adminCat = cat; UI._adminSection = ADMIN_SECTIONS[cat][0]; UI._renderAdminNav(); UI._switchAdminSection(UI._adminSection); }, // ── Render navigation state ─────────────── _renderAdminNav() { // Highlight active category document.querySelectorAll('.admin-cat').forEach(btn => { btn.classList.toggle('active', btn.dataset.cat === UI._adminCat); }); // Populate sidebar const sidebar = document.getElementById('adminSidebar'); sidebar.innerHTML = ADMIN_SECTIONS[UI._adminCat].map(sec => `` ).join(''); // Wire sidebar clicks sidebar.querySelectorAll('button').forEach(btn => { btn.addEventListener('click', () => UI._switchAdminSection(btn.dataset.section)); }); }, // ── Section switch (shows content + lazy-loads) ── async _switchAdminSection(section) { UI._adminSection = section; // Update sidebar highlight document.querySelectorAll('.admin-sidebar button').forEach(btn => { btn.classList.toggle('active', btn.dataset.section === section); }); // Hide all, show target document.querySelectorAll('.admin-section-content').forEach(c => c.style.display = 'none'); const panel = document.getElementById(`admin${section.charAt(0).toUpperCase() + section.slice(1)}Tab`); if (panel) panel.style.display = ''; // Lazy-load data await ADMIN_LOADERS[section]?.(); }, // ── Backward compat: switchAdminTab still works ── // (called by some inline onclick handlers and admin-handlers.js) async switchAdminTab(tab) { const cat = _adminCatForSection(tab); if (document.getElementById('adminPanel').style.display !== 'flex') { UI.openAdmin(cat, tab); } else { if (cat !== UI._adminCat) { UI._adminCat = cat; UI._renderAdminNav(); } await UI._switchAdminSection(tab); } }, openTeamAdmin() { const adminTeams = (UI._myTeams || []).filter(t => t.my_role === 'admin'); if (adminTeams.length === 0) { UI.toast('You are not an admin of any teams', 'error'); return; } openModal('teamAdminModal'); if (adminTeams.length === 1) { // Skip picker, go directly to the team UI.openTeamManage(adminTeams[0].id, adminTeams[0].name); } else { // Show team picker document.getElementById('teamAdminTitle').textContent = 'Team Management'; document.getElementById('teamAdminPicker').style.display = ''; document.getElementById('teamAdminContent').style.display = 'none'; document.getElementById('teamAdminPickerList').innerHTML = adminTeams.map(t => `
${esc(t.name)} admin Manage →
${t.description ? `
${esc(t.description)}
` : ''}
${t.member_count} member${t.member_count !== 1 ? 's' : ''}
`).join(''); } }, closeTeamAdmin() { closeModal('teamAdminModal'); }, switchTeamTab(tab) { const tabs = document.querySelectorAll('#teamAdminTabs .admin-tab'); tabs.forEach(t => { t.classList.remove('active'); if (t.dataset.ttab === tab) t.classList.add('active'); }); document.querySelectorAll('.team-tab-content').forEach(c => c.style.display = 'none'); const panel = document.getElementById(`teamTab${tab.charAt(0).toUpperCase() + tab.slice(1)}`); if (panel) panel.style.display = ''; // Lazy-load tab data const teamId = UI._managingTeamId; if (tab === 'members') UI.loadTeamManageMembers(teamId); if (tab === 'providers') UI.loadTeamManageProviders(teamId); if (tab === 'personas') UI.loadTeamManagePersonas(teamId); if (tab === 'usage') UI.loadTeamUsage(); if (tab === 'activity') UI.loadTeamAuditLog(1); if (tab === 'settings') UI.loadTeamManageSettings(teamId); if (tab === 'knowledge') { if (typeof KnowledgeUI === 'undefined') { console.error('[TeamAdmin] KnowledgeUI not loaded — check js/knowledge-ui.js'); const c = document.getElementById('teamKBContent'); if (c) c.innerHTML = '
Knowledge UI failed to load. Check browser console.
'; } else { KnowledgeUI.openTeamPanel(teamId); } } if (tab === 'groups') UI.loadTeamManageGroups(teamId); }, async loadAdminUsers(quiet) { const el = document.getElementById('adminUserList'); if (!quiet) el.innerHTML = '
Loading...
'; try { const resp = await API.adminListUsers(); const users = resp.users || resp.data || []; if (!users.length) { el.innerHTML = '
No users
'; return; } const rows = users.map(u => { const initial = (u.display_name || u.username || '?')[0].toUpperCase(); const name = esc(u.display_name || u.username); const handle = '@' + esc(u.username); const roleCls = u.role === 'admin' ? 'badge-admin' : u.role === 'service' ? 'badge-service' : 'badge-user'; const isPending = !u.is_active; const statusCls = isPending ? 'badge-warning' : 'badge-active'; const statusLabel = isPending ? 'pending' : 'active'; const teams = (u.teams || []).map(t => esc(t.team_name)).join(', ') || '\u2014'; const lastSeen = u.last_login_at ? _relativeTime(u.last_login_at) : '\u2014'; return `
${initial}
${name}
${handle}
${u.role} ${statusLabel} ${teams} ${lastSeen} ${isPending ? `` : `` } `; }).join(''); el.innerHTML = ` ${rows}
UserRoleStatusTeamsLast Seen
`; // Approve form placeholder (injected per-user when needed) } catch (e) { el.innerHTML = `
${esc(e.message)}
`; } }, async loadAdminStats() { const el = document.getElementById('adminStats'); el.innerHTML = '
Loading...
'; try { const s = await API.adminGetStats(); const labels = { users: 'Users', channels: 'Channels', messages: 'Messages', models: 'Models', providers: 'Providers', personas: 'Personas' }; el.innerHTML = Object.entries(s).map(([k, v]) => `
${labels[k] || k.replace(/_/g, ' ')}
${typeof v === 'number' ? v.toLocaleString() : esc(String(v))}
`).join(''); } catch (e) { el.innerHTML = `
${esc(e.message)}
`; } }, // ── Admin: Roles ──────────────────────── async loadAdminRoles() { const el = document.getElementById('adminRolesContent'); if (!el) return; if (!UI._adminRoleConfig) { UI._adminRoleConfig = renderRoleConfig(el, { scope: 'admin', showFallback: true, modelIdField: 'model_id', modelNameField: 'display_name', providerIdField: 'provider_config_id', fetchProviders: async () => { const configs = await API.adminListGlobalConfigs(); return configs.configs || configs || []; }, fetchModels: async () => { const models = await API.adminListModels(); return models.models || models.data || models || []; }, fetchRoles: () => API.adminListRoles(), onSave: async (roleId, config) => { await API.adminUpdateRole(roleId, config); }, onTest: (roleId) => API.adminTestRole(roleId), }); } await UI._adminRoleConfig.refresh(); }, // ── Admin: Usage ──────────────────────── async loadAdminUsage() { // Stat cards + bar chart const cardsEl = document.getElementById('adminUsageCards'); const chartEl = document.getElementById('adminUsageChart'); // Fetch 7-day usage for cards and chart if (cardsEl) { try { const weekData = await API.adminGetUsage({ period: '7d', group_by: 'day' }); const buckets = weekData.data || weekData.buckets || []; const totalReqs = buckets.reduce((s, b) => s + (b.request_count || 0), 0); const totalIn = buckets.reduce((s, b) => s + (b.input_tokens || 0), 0); const totalOut = buckets.reduce((s, b) => s + (b.output_tokens || 0), 0); const totalTokens = totalIn + totalOut; const tokenStr = totalTokens > 1e6 ? (totalTokens / 1e6).toFixed(1) + 'M' : totalTokens > 1e3 ? (totalTokens / 1e3).toFixed(0) + 'K' : totalTokens; const userSet = new Set(buckets.filter(b => b.user_id).map(b => b.user_id)); const avgMs = buckets.reduce((s, b) => s + (b.avg_latency_ms || 0), 0); const avgLatency = buckets.length ? (avgMs / buckets.length / 1000).toFixed(1) + 's' : '—'; cardsEl.innerHTML = `
Total Requests
${totalReqs.toLocaleString()}
Last 7 days
Tokens Used
${tokenStr}
Input + output
Active Users
${userSet.size || '—'}
Last 7 days
Avg Response
${avgLatency}
p50 latency
`; // Bar chart — aggregate by day label if (chartEl && buckets.length > 0) { const dayMap = {}; buckets.forEach(b => { const d = new Date(b.window_start || b.date || b.bucket); const label = d.toLocaleDateString('en', { weekday: 'short' }); dayMap[label] = (dayMap[label] || 0) + (b.request_count || 0); }); const days = Object.entries(dayMap); const maxVal = Math.max(...days.map(d => d[1]), 1); chartEl.innerHTML = `
Requests per Day
${days.map(([label, val]) => { const pct = Math.round(val / maxVal * 100); return `
${val}
${label}
`; }).join('')}
`; } } catch (e) { cardsEl.innerHTML = ''; if (chartEl) chartEl.innerHTML = ''; } } // Usage breakdown via primitive (preserves existing period/group-by controls) const totalsEl = document.getElementById('adminUsageTotals'); if (!totalsEl) return; let usageContainer = document.getElementById('_adminUsageContainer'); if (!usageContainer) { usageContainer = document.createElement('div'); usageContainer.id = '_adminUsageContainer'; const resultsEl = document.getElementById('adminUsageResults'); totalsEl.parentElement.insertBefore(usageContainer, totalsEl); usageContainer.appendChild(totalsEl); if (resultsEl) usageContainer.appendChild(resultsEl); } if (!UI._adminUsageDash) { UI._adminUsageDash = renderUsageDashboard(usageContainer, { apiFetch: (p) => API.adminGetUsage(p), periodElId: 'usagePeriod', groupByElId: 'usageGroupBy', compact: false, showUserColumn: true, }); } await UI._adminUsageDash.refresh(); // Pricing table const pricingEl = document.getElementById('adminPricingTable'); if (pricingEl) { try { const pricing = await API.adminListPricing(); const entries = pricing || []; if (entries.length === 0) { pricingEl.innerHTML = '
No pricing configured. Sync providers to populate from catalog.
'; } else { pricingEl.innerHTML = ` ${entries.map(p => ``).join('')}
Model Input $/M Output $/M Source
${esc(p.model_id)} ${p.input_per_m != null ? '$' + Number(p.input_per_m).toFixed(4) : '—'} ${p.output_per_m != null ? '$' + Number(p.output_per_m).toFixed(4) : '—'} ${p.source}
`; } } catch (e) { pricingEl.innerHTML = `
${esc(e.message)}
`; } } }, async loadAdminExtensions() { const el = document.getElementById('adminExtensionsList'); el.innerHTML = '
Loading...
'; try { const resp = await API._get('/api/v1/admin/extensions'); const exts = resp.data || []; this._adminExtensions = exts; if (exts.length === 0) { el.innerHTML = '
No extensions installed
'; return; } el.innerHTML = ` ${exts.map(ext => { const statusBadge = ext.is_enabled ? 'enabled' : 'disabled'; const systemBadge = ext.is_system ? ' system' : ''; return ``; }).join('')}
ExtensionStatusTierVersion
${esc(ext.name)}${systemBadge}
${esc(ext.description || 'No description')}${ext.author ? ' · by ' + esc(ext.author) : ''}
${statusBadge} ${esc(ext.tier)} v${esc(ext.version)}
`; } catch (e) { el.innerHTML = '
Failed to load extensions
'; } }, _auditPage: 1, _auditPerPage: 30, async loadAuditLog(page) { if (page) UI._auditPage = page; const el = document.getElementById('adminAuditList'); el.innerHTML = '
Loading...
'; // Populate action filter on first load const actionSel = document.getElementById('auditFilterAction'); if (actionSel && actionSel.options.length <= 1) { try { const resp = await API.adminListAuditActions(); (resp.actions || []).forEach(a => { const opt = document.createElement('option'); opt.value = a; opt.textContent = a; actionSel.appendChild(opt); }); } catch (e) { /* optional */ } } const params = { page: UI._auditPage, per_page: UI._auditPerPage, action: document.getElementById('auditFilterAction')?.value || '', resource_type: document.getElementById('auditFilterResource')?.value || '', }; try { const resp = await API.adminListAudit(params); const entries = resp.data || []; const total = resp.total || 0; const totalPages = Math.ceil(total / UI._auditPerPage); if (entries.length === 0) { el.innerHTML = '
No audit entries found
'; } else { el.innerHTML = ` ${entries.map(e => { const ts = new Date(e.created_at); const timeStr = ts.toLocaleDateString() + ' ' + ts.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'}); const actor = e.actor_name || 'system'; const meta = UI._formatAuditMeta(e.metadata); return ``; }).join('')}
ActionActorResourceDetailsTimeIP
${esc(e.action)} ${esc(actor)} ${esc(e.resource_type)}${e.resource_id ? ':' + esc(e.resource_id).slice(0,8) : ''} ${meta || '—'} ${timeStr} ${e.ip_address ? esc(e.ip_address) : '—'}
`; } // Pagination const pgEl = document.getElementById('auditPagination'); if (totalPages > 1) { pgEl.style.display = 'flex'; document.getElementById('auditPageInfo').textContent = `Page ${UI._auditPage} of ${totalPages} (${total} entries)`; document.getElementById('auditPrevBtn').disabled = UI._auditPage <= 1; document.getElementById('auditNextBtn').disabled = UI._auditPage >= totalPages; } else { pgEl.style.display = 'none'; } } catch (e) { el.innerHTML = `
${esc(e.message)}
`; } }, _formatAuditMeta(metaStr) { try { const m = typeof metaStr === 'string' ? JSON.parse(metaStr) : metaStr; if (!m || Object.keys(m).length === 0) return ''; return Object.entries(m).map(([k,v]) => `${esc(k)}=${esc(String(v))}`).join(' · '); } catch { return ''; } }, async loadAdminProviders(quiet) { const el = document.getElementById('adminProviderList'); const formEl = document.getElementById('adminAddProviderForm'); // Initialize admin provider form primitive (once) — now renders inside modal body if (!UI._adminProvForm && formEl) { UI._adminProvForm = renderProviderForm(formEl, { prefix: 'adminProv', showPrivate: true, showDefaultModel: true, onSubmit: async (vals, isEdit) => { try { if (isEdit) { const updates = {}; if (vals.name) updates.name = vals.name; if (vals.endpoint) updates.endpoint = vals.endpoint; if (vals.api_key) updates.api_key = vals.api_key; updates.model_default = vals.model_default; updates.is_private = vals.is_private || false; await API.adminUpdateGlobalConfig(vals._editId, updates); UI.toast('Provider updated', 'success'); } else { if (!vals.name || !vals.endpoint || !vals.api_key) { UI.toast('Name, endpoint, and API key required', 'warning'); return; } await API.adminCreateGlobalConfig(vals.name, vals.provider, vals.endpoint, vals.api_key, vals.model_default, vals.is_private || false); UI.toast('Provider added', 'success'); } closeModal('providerFormModal'); UI._adminProvForm.setCreateMode(); UI.loadAdminProviders(true); } catch (e) { UI.toast(e.message, 'error'); } }, onCancel: () => { closeModal('providerFormModal'); UI._adminProvForm.setCreateMode(); }, }); } if (!quiet) el.innerHTML = '
Loading...
'; try { // Fetch providers, health, and models in parallel const [configsRaw, healthRaw, modelsRaw] = await Promise.all([ API.adminListGlobalConfigs(), API.adminGetAllProviderHealth().catch(() => ({ providers: [] })), API.adminListModels().catch(() => ({ models: [] })), ]); const configs = Array.isArray(configsRaw) ? configsRaw : (configsRaw.configs || configsRaw.providers || configsRaw.data || []); const healthList = healthRaw.providers || healthRaw.data || healthRaw || []; const models = modelsRaw.models || modelsRaw.data || modelsRaw || []; // Build health lookup by provider_config_id const healthMap = {}; (Array.isArray(healthList) ? healthList : []).forEach(h => { healthMap[h.provider_config_id] = h; }); // Count models per provider const modelCounts = {}; (Array.isArray(models) ? models : []).forEach(m => { const pid = m.provider_config_id; if (pid) modelCounts[pid] = (modelCounts[pid] || 0) + 1; }); // Cache for edit UI._providerCache = {}; configs.forEach(c => { UI._providerCache[c.id] = c; }); if (!configs.length) { el.innerHTML = '
No providers configured \u2014 click + Add above
'; return; } el.innerHTML = configs.map(c => { const health = healthMap[c.id]; const status = health ? health.status : 'unknown'; const statusCls = status === 'healthy' ? 'badge-healthy' : status === 'degraded' ? 'badge-degraded' : status === 'unhealthy' ? 'badge-unhealthy' : 'badge-unknown'; const count = modelCounts[c.id] || 0; const scope = c.is_private ? 'Private' : (c.team_id ? 'Team' : ''); return `
${esc(c.name)}${scope ? ' (' + scope + ')' : ''}
${status}
${esc(c.provider || '')} ${count ? `${count} model${count !== 1 ? 's' : ''}` : ''}
`; }).join(''); } catch (e) { el.innerHTML = `
${esc(e.message)}
`; } // Delegate card clicks → open edit modal if (!el._delegated) { el._delegated = true; el.addEventListener('click', (e) => { const card = e.target.closest('.provider-card'); if (!card) return; const id = card.dataset.id; const prov = UI._providerCache?.[id]; if (!prov || !UI._adminProvForm) return; UI._adminProvForm.setEditMode(prov.id, prov); document.getElementById('providerFormTitle').textContent = 'Edit Provider'; openModal('providerFormModal'); }); } }, async loadAdminModels(quiet) { const el = document.getElementById('adminModelList'); if (!quiet) el.innerHTML = '
Loading...
'; try { const data = await API.adminListModels(); const list = data.models || data.data || data || []; const arr = Array.isArray(list) ? list : []; if (!arr.length) { el.innerHTML = '
No models — add a provider first, then click Fetch Models
'; return; } el.innerHTML = ` ${arr.map(m => { const caps = m.capabilities || {}; const badges = renderCapBadges(caps, { compact: true }); const vis = m.visibility || (m.is_enabled === true ? 'enabled' : m.is_enabled === false ? 'disabled' : 'disabled'); const visLabel = vis === 'enabled' ? '✓ Enabled' : vis === 'team' ? '👥 Team' : 'Disabled'; const visClass = vis === 'enabled' ? 'enabled' : vis === 'team' ? 'team' : ''; return ``; }).join('')}
ModelProviderCapabilitiesVisibility
${esc(m.model_id || m.id)} ${esc(m.provider_name || '')} ${badges}
`; } catch (e) { el.innerHTML = `
${esc(e.message)}
`; } }, async loadAdminPersonas(quiet) { const el = document.getElementById('adminPersonaList'); if (!quiet) el.innerHTML = '
Loading...
'; try { const data = await API.adminListPersonas(); const list = data.personas || []; // Ensure form is initialized for dropdown population ensureAdminPersonaForm(); // Populate model dropdown from admin model list (all synced models) const modelSel = _adminPersonaForm?.getModelSelect(); if (modelSel) { modelSel.innerHTML = ''; try { const modelData = await API.adminListModels(); const allModels = modelData.models || modelData.data || []; const arr = Array.isArray(allModels) ? allModels : []; arr.forEach(m => { const opt = document.createElement('option'); const mid = m.model_id || m.id; opt.value = mid; opt.textContent = mid + (m.provider_name ? ` (${m.provider_name})` : ''); modelSel.appendChild(opt); }); } catch (e) { console.warn('Failed to load models for persona form:', e.message); } } // Populate config dropdown const cfgSel = _adminPersonaForm?.getConfigSelect(); if (cfgSel && cfgSel.options.length <= 1) { try { const cfgData = await API.adminListGlobalConfigs(); const cfgs = cfgData.configs || cfgData.data || []; (Array.isArray(cfgs) ? cfgs : []).forEach(c => { const opt = document.createElement('option'); opt.value = c.id; opt.textContent = c.name + ' (' + c.provider + ')'; cfgSel.appendChild(opt); }); } catch (e) { /* optional */ } } UI._personaCache = {}; if (!list.length) { el.innerHTML = '
No personas — create one to give users preconfigured model experiences
'; return; } el.innerHTML = ` ${list.map(p => { UI._personaCache[p.id] = p; const avatarEl = p.avatar ? `` : ''; let scopeBadge = ''; if (p.scope === 'global') scopeBadge = 'global'; else if (p.scope === 'team' && p.team_name) scopeBadge = `${esc(p.team_name)}`; else if (p.scope === 'personal') scopeBadge = 'personal'; const creator = p.creator_name ? `
by ${esc(p.creator_name)}
` : ''; return ``; }).join('')}
PersonaScopeModelStatus
${avatarEl}
${esc(p.name)}
${p.description ? `
${esc(p.description)}
` : creator}
${scopeBadge} ${esc(p.base_model_id)} ${p.is_active ? 'active' : 'inactive'}
`; } catch (e) { el.innerHTML = `
${esc(e.message)}
`; } }, // ── Teams Admin ───────────────────────── _teamEditId: null, async loadAdminTeams(quiet) { const el = document.getElementById('adminTeamList'); const detail = document.getElementById('adminTeamDetail'); if (!quiet) { el.innerHTML = '
Loading...
'; detail.style.display = 'none'; el.style.display = ''; closeModal('createTeamModal'); } try { const resp = await API.adminListTeams(); const teams = resp.data || []; if (!teams.length) { el.innerHTML = '
No teams yet — create one to organize users and set policies
'; return; } el.innerHTML = ` ${teams.map(t => ``).join('')}
TeamStatusMembers
${esc(t.name)}
${esc(t.description || 'No description')}
${t.is_active ? 'active' : 'inactive'} ${t.member_count} member${t.member_count !== 1 ? 's' : ''}
`; } catch (e) { el.innerHTML = `
${esc(e.message)}
`; } }, async openTeamDetail(teamId, teamName) { this._teamEditId = teamId; document.getElementById('adminTeamList').style.display = 'none'; closeModal('createTeamModal'); const detail = document.getElementById('adminTeamDetail'); detail.style.display = ''; document.getElementById('adminTeamDetailName').textContent = teamName; document.getElementById('adminAddMemberForm').style.display = 'none'; // Load team settings for policy checkboxes try { const team = await API.adminGetTeam(teamId); const settings = typeof team.settings === 'string' ? JSON.parse(team.settings || '{}') : (team.settings || {}); document.getElementById('adminTeamPrivatePolicy').checked = !!settings.require_private_providers; // allow_team_providers defaults to true if not explicitly set document.getElementById('adminTeamAllowProviders').checked = settings.allow_team_providers !== false; } catch (e) { /* proceed with defaults */ } await this.loadTeamMembers(teamId); }, async loadTeamMembers(teamId) { const el = document.getElementById('adminMemberList'); el.innerHTML = '
Loading...
'; try { const resp = await API.adminListMembers(teamId); const members = resp.data || []; if (!members.length) { el.innerHTML = '
No members — add users to this team
'; return; } el.innerHTML = ` ${members.map(m => ``).join('')}
MemberRole
${esc(m.display_name || m.email)}
${esc(m.email)}
${m.user_role === 'admin' ? 'sys-admin' : ''}
`; } catch (e) { el.innerHTML = `
${esc(e.message)}
`; } }, async loadMemberUserDropdown(teamId) { const sel = document.getElementById('adminMemberUser'); sel.innerHTML = ''; try { const resp = await API.adminListUsers(1, 200); const users = resp.users || resp.data || []; // Also get existing members to exclude const memberResp = await API.adminListMembers(teamId); const existingIds = new Set((memberResp.data || []).map(m => m.user_id)); const available = users.filter(u => u.is_active && !existingIds.has(u.id)); sel.innerHTML = '' + available.map(u => ``).join(''); } catch (e) { sel.innerHTML = ``; } }, // ── Groups Admin ──────────────────────── _groupEditId: null, async loadAdminGroups(quiet) { const el = document.getElementById('adminGroupList'); const detail = document.getElementById('adminGroupDetail'); if (!quiet) { el.innerHTML = '
Loading...
'; detail.style.display = 'none'; el.style.display = ''; closeModal('createGroupModal'); } try { const resp = await API.adminListGroups(); const groups = resp.data || []; if (!groups.length) { el.innerHTML = '
No groups yet — create one to control access to personas and knowledge bases
'; return; } el.innerHTML = ` ${groups.map(g => { const scopeBadge = g.scope === 'global' ? 'global' : 'team'; return ``; }).join('')}
GroupScopeMembers
${esc(g.name)}
${esc(g.description || 'No description')}
${scopeBadge} ${g.member_count} member${g.member_count !== 1 ? 's' : ''}
`; } catch (e) { el.innerHTML = `
${esc(e.message)}
`; } }, async openGroupDetail(groupId, groupName) { this._groupEditId = groupId; document.getElementById('adminGroupList').style.display = 'none'; closeModal('createGroupModal'); const detail = document.getElementById('adminGroupDetail'); detail.style.display = ''; document.getElementById('adminGroupDetailName').textContent = groupName; document.getElementById('adminAddGroupMemberForm').style.display = 'none'; // Load group details for meta line try { const g = await API.adminGetGroup(groupId); const scope = g.scope === 'global' ? 'Global group' : 'Team-scoped group'; document.getElementById('adminGroupDetailMeta').textContent = `${scope} · ${g.member_count} member${g.member_count !== 1 ? 's' : ''}`; } catch (e) { /* proceed */ } await this.loadGroupMembers(groupId); }, async loadGroupMembers(groupId) { const el = document.getElementById('adminGroupMemberList'); el.innerHTML = '
Loading...
'; try { const resp = await API.adminListGroupMembers(groupId); const members = resp.data || []; if (!members.length) { el.innerHTML = '
No members — add users to this group
'; return; } el.innerHTML = ` ${members.map(m => ``).join('')}
MemberAdded
${esc(m.display_name || m.username || m.email)}
${esc(m.email)}
${new Date(m.added_at).toLocaleDateString()}
`; } catch (e) { el.innerHTML = `
${esc(e.message)}
`; } }, async loadGroupMemberDropdown(groupId) { const sel = document.getElementById('adminGroupMemberUser'); sel.innerHTML = ''; try { const resp = await API.adminListUsers(1, 200); const users = resp.users || resp.data || []; // Exclude existing members const memberResp = await API.adminListGroupMembers(groupId); const existingIds = new Set((memberResp.data || []).map(m => m.user_id)); const available = users.filter(u => u.is_active && !existingIds.has(u.id)); sel.innerHTML = '' + available.map(u => ``).join(''); } catch (e) { sel.innerHTML = ``; } }, // ── Grant Picker (shared for Personas + KBs) ── async openGrantPicker(resourceType, resourceId, resourceName, anchorSelector) { // Fetch current grant + all groups for the picker const [grantResp, groupsResp] = await Promise.all([ API.adminGetGrant(resourceType, resourceId), API.adminListGroups(), ]); const grant = grantResp; const groups = (groupsResp.data || []); const currentScope = grant.grant_scope || 'team_only'; const currentGroups = new Set(grant.granted_groups || []); const html = `
Access: ${esc(resourceName)}
${groups.length ? groups.map(g => ``).join('') : 'No groups exist yet — create one in People → Groups'}
`; // Remove any existing picker, then insert after the anchor row document.querySelectorAll('.grant-picker-active').forEach(el => el.remove()); const row = document.querySelector(anchorSelector); if (row) { const wrapper = document.createElement('div'); wrapper.className = 'grant-picker-active'; wrapper.innerHTML = html; row.after(wrapper); // Wire scope toggle const scopeSel = document.getElementById(`grantScope_${resourceId}`); const groupsDiv = document.getElementById(`grantGroups_${resourceId}`); scopeSel?.addEventListener('change', () => { groupsDiv.style.display = scopeSel.value === 'groups' ? '' : 'none'; }); } }, // ── Provider Health Dashboard (v0.22.3) ────── async loadAdminHealth() { const cardsEl = document.getElementById('adminHealthCards'); const el = document.getElementById('adminHealthContent'); if (cardsEl) cardsEl.innerHTML = ''; el.innerHTML = '
Loading health data...
'; try { const data = await API.adminGetAllProviderHealth(); const providers = data.data || []; if (providers.length === 0) { el.innerHTML = '
No health data yet — health metrics are collected after the first completion request.
'; return; } // Stat cards const total = providers.length; const healthy = providers.filter(p => p.status === 'healthy').length; const degraded = providers.filter(p => p.status === 'degraded').length; const down = providers.filter(p => p.status === 'down' || p.status === 'unhealthy').length; const avgLat = providers.reduce((s, p) => s + (p.avg_latency_ms || 0), 0); const avgLatStr = total > 0 ? Math.round(avgLat / total) + 'ms' : '—'; if (cardsEl) { cardsEl.innerHTML = `
Providers
${total}
All configured
Healthy
${healthy}
${total ? Math.round(healthy / total * 100) : 0}%
Degraded
${degraded + down}
${total ? Math.round((degraded + down) / total * 100) : 0}%
Avg Latency
${avgLatStr}
Last hour
`; } // Table const rows = providers.map(p => { const name = esc(p.provider_name || p.provider_config_id?.slice(0, 8) || '—'); const statusCls = p.status === 'healthy' ? 'badge-healthy' : p.status === 'degraded' ? 'badge-degraded' : p.status === 'down' || p.status === 'unhealthy' ? 'badge-unhealthy' : 'badge-unknown'; const latency = p.avg_latency_ms != null ? p.avg_latency_ms + 'ms' : '—'; const errorRate = p.error_rate != null ? (100 - p.error_rate * 100).toFixed(1) + '%' : '—'; const reqs = p.request_count ?? 0; const errCount = (p.error_count || 0) + (p.timeout_count || 0); const lastErr = p.last_error ? `
${esc(p.last_error)}
` : '—'; return ` ${name} ${esc(p.status || 'unknown')} ${latency} ${errorRate} ${reqs} ${errCount} ${lastErr} `; }).join(''); el.innerHTML = ` ${rows}
ProviderStatusLatencyUptimeRequestsErrorsLast Error
`; } catch (e) { el.innerHTML = `
Failed to load health data: ${esc(e.message)}
`; } }, // ── Routing Policies (v0.22.3) ─────────────── async loadAdminRouting() { const listEl = document.getElementById('adminRoutingPolicyList'); const formEl = document.getElementById('adminRoutingForm'); listEl.innerHTML = '
Loading policies...
'; // Wire add button const addBtn = document.getElementById('adminAddRoutingPolicyBtn'); addBtn?.addEventListener('click', () => UI._showRoutingForm(null), { once: true }); // Wire test button const testBtn = document.getElementById('routingTestBtn'); testBtn?.addEventListener('click', () => UI._runRoutingTest(), { once: true }); try { const data = await API.adminListRoutingPolicies(); const policies = data.data || []; if (policies.length === 0) { listEl.innerHTML = '
No routing policies — requests go to the user\'s default provider config.
'; return; } const typeLabels = { provider_prefer: 'Provider Prefer', team_route: 'Team Route', cost_limit: 'Cost Limit', model_alias: 'Model Alias' }; listEl.innerHTML = ` ${policies.map(p => ``).join('')}
PolicyTypeScopePriorityStatus
${esc(p.name)} ${esc(typeLabels[p.policy_type] || p.policy_type)} ${p.scope}${p.team_id ? ' · ' + p.team_id.slice(0, 8) : ''} ${p.priority} ${p.is_active ? 'active' : 'inactive'}
`; } catch (e) { listEl.innerHTML = `
Failed to load routing policies: ${esc(e.message)}
`; } }, async _showRoutingForm(policyId) { const formEl = document.getElementById('adminRoutingForm'); const isEdit = !!policyId; let policy = { name: '', scope: 'global', priority: 100, policy_type: 'provider_prefer', is_active: true, config: {} }; if (isEdit) { try { policy = await API.adminGetRoutingPolicy(policyId); } catch (e) { UI.toast(e.message, 'error'); return; } } const cfg = policy.config || {}; formEl.style.display = ''; formEl.innerHTML = `

${isEdit ? 'Edit' : 'New'} Routing Policy

`; document.getElementById('rpScope').addEventListener('change', (e) => { document.getElementById('rpTeamGroup').style.display = e.target.value === 'team' ? '' : 'none'; }); document.getElementById('rpCancelBtn').addEventListener('click', () => { formEl.style.display = 'none'; }); document.getElementById('rpSaveBtn').addEventListener('click', async () => { let config; try { config = JSON.parse(document.getElementById('rpConfig').value || '{}'); } catch (e) { UI.toast('Invalid JSON in config field', 'error'); return; } const payload = { name: document.getElementById('rpName').value, priority: parseInt(document.getElementById('rpPriority').value) || 100, policy_type: document.getElementById('rpType').value, scope: document.getElementById('rpScope').value, team_id: document.getElementById('rpScope').value === 'team' ? document.getElementById('rpTeamId').value : null, is_active: document.getElementById('rpActive').checked, config, }; try { if (isEdit) { await API.adminUpdateRoutingPolicy(policyId, payload); } else { await API.adminCreateRoutingPolicy(payload); } UI.toast(isEdit ? 'Policy updated' : 'Policy created', 'success'); formEl.style.display = 'none'; UI.loadAdminRouting(); } catch (e) { UI.toast(e.message, 'error'); } }); }, async _toggleRoutingPolicy(id, active) { try { const policy = await API.adminGetRoutingPolicy(id); policy.is_active = active; await API.adminUpdateRoutingPolicy(id, policy); UI.toast(active ? 'Policy enabled' : 'Policy disabled', 'success'); UI.loadAdminRouting(); } catch (e) { UI.toast(e.message, 'error'); } }, async _deleteRoutingPolicy(id, name) { if (!await showConfirm(`Delete routing policy "${name}"?`)) return; try { await API.adminDeleteRoutingPolicy(id); UI.toast('Policy deleted', 'success'); UI.loadAdminRouting(); } catch (e) { UI.toast(e.message, 'error'); } }, async _runRoutingTest() { const model = document.getElementById('routingTestModel').value; const userId = document.getElementById('routingTestUser').value || API.user?.id; const resultEl = document.getElementById('routingTestResult'); if (!model) { UI.toast('Model is required', 'warning'); return; } resultEl.style.display = ''; resultEl.innerHTML = '
Evaluating...
'; try { const data = await API.adminTestRouting(model, userId); const dec = data.decision; const candidates = data.candidates || []; resultEl.innerHTML = `
Decision: ${dec ? `${esc(dec.policy_name || 'no policy')} → ${esc(dec.selected?.slice(0, 8) || '—')}` : 'No routing applied'} (${candidates.length} candidates, ${data.policies || 0} policies evaluated)
${candidates.map((c, i) => `
${i + 1}. ${esc(c.provider_id)} / ${esc(c.config_id?.slice(0, 8) || '—')} ${esc(c.status || 'unknown')} ${c.reason ? `${esc(c.reason)}` : ''}
`).join('')}
`; } catch (e) { resultEl.innerHTML = `
Test failed: ${esc(e.message)}
`; } }, // ── Capability Overrides (v0.22.3) ─────────── async loadAdminCapabilities() { const el = document.getElementById('adminCapabilityList'); el.innerHTML = '
Loading overrides...
'; try { const data = await API.adminListCapabilityOverrides(); const overrides = data.data || []; if (overrides.length === 0) { el.innerHTML = '
No capability overrides — all models use auto-detected capabilities from catalog sync or heuristic inference.
'; return; } el.innerHTML = `${overrides.map(o => ``).join('')}
ModelProvider ConfigFieldValue
${esc(o.model_id)} ${esc(o.provider_config_id?.slice(0, 8) || 'any')} ${esc(o.field)} ${esc(String(o.value))}
`; } catch (e) { el.innerHTML = `
Failed to load overrides: ${esc(e.message)}
`; } }, async _deleteCapOverride(modelId, overrideId) { try { await API.adminDeleteModelCapability(modelId, overrideId); UI.toast('Override deleted', 'success'); UI.loadAdminCapabilities(); } catch (e) { UI.toast(e.message, 'error'); } }, async loadAdminSettings() { try { const data = await API.adminGetSettings(); // v0.9: { settings: { banner: {...}, ... }, policies: { allow_registration: "true", ... } } const settings = data.settings || {}; const policies = data.policies || {}; // Helper to read from settings map (JSONB values) const getSetting = (key, fallback) => { const v = settings[key]; if (v === undefined || v === null) return fallback; // Unwrap {value: X} wrapper if present return (v && typeof v === 'object' && 'value' in v) ? v.value : v; }; // Registration (policy: allow_registration) document.getElementById('adminRegToggle').checked = policies.allow_registration === 'true'; // Admin system prompt (global_settings) const sysPrompt = getSetting('system_prompt', {}) || {}; document.getElementById('adminSystemPrompt').value = sysPrompt.content || ''; // Registration default state (policy: default_user_active → 'true' means auto-active) const defaultActive = policies.default_user_active === 'true'; document.getElementById('adminRegDefaultState').value = defaultActive ? 'active' : 'pending'; // User providers / BYOK (policy: allow_user_byok) document.getElementById('adminUserProvidersToggle').checked = policies.allow_user_byok === 'true'; // User personas (policy: allow_user_personas) document.getElementById('adminUserPersonasToggle').checked = policies.allow_user_personas === 'true'; // KB direct access (policy: kb_direct_access — v0.17.0) const kbDirectEl = document.getElementById('adminKBDirectAccessToggle'); if (kbDirectEl) kbDirectEl.checked = policies.kb_direct_access === 'true'; // Default model (policy: default_model) — populate from current model list const defModelSel = document.getElementById('adminDefaultModel'); if (defModelSel) { defModelSel.innerHTML = ''; let modelList = typeof App !== 'undefined' && App.models ? App.models : []; // On admin surface, App isn't loaded — fetch from admin API if (modelList.length === 0 && typeof API !== 'undefined' && API.adminListModels) { try { const mr = await API.adminListModels(); modelList = (mr.models || mr.data || []).map(m => ({ id: m.model_id || m.id, baseModelId: m.model_id || m.id, name: m.display_name || m.model_id || m.id, provider: m.provider_name || '', hidden: !!m.hidden, })); } catch (e) { /* non-critical */ } } modelList.filter(m => !m.hidden).forEach(m => { const opt = document.createElement('option'); opt.value = m.baseModelId || m.id; opt.textContent = (m.name || m.id) + (m.provider ? ` (${m.provider})` : ''); defModelSel.appendChild(opt); }); defModelSel.value = policies.default_model || ''; } // Banner (global_settings) const banner = getSetting('banner', {}) || {}; document.getElementById('adminBannerEnabled').checked = !!banner.enabled; document.getElementById('adminBannerText').value = banner.text || ''; document.getElementById('adminBannerBg').value = banner.bg || '#007a33'; document.getElementById('adminBannerBgHex').value = banner.bg || '#007a33'; document.getElementById('adminBannerFg').value = banner.fg || '#ffffff'; document.getElementById('adminBannerFgHex').value = banner.fg || '#ffffff'; document.getElementById('bannerConfigFields').style.display = banner.enabled ? '' : 'none'; UI.updateBannerPreview(); // Vault / Encryption status const vaultEl = document.getElementById('adminVaultStatus'); // Web Search config (global_settings) const searchCfg = getSetting('search_config', {}) || {}; document.getElementById('adminSearchProvider').value = searchCfg.provider || 'duckduckgo'; document.getElementById('adminSearchEndpoint').value = searchCfg.endpoint || ''; document.getElementById('adminSearchApiKey').value = searchCfg.api_key || ''; document.getElementById('adminSearchMaxResults').value = String(searchCfg.max_results || 5); document.getElementById('searxngConfigFields').style.display = (searchCfg.provider === 'searxng') ? '' : 'none'; // Auto-Compaction (global_settings) const compactionCfg = getSetting('auto_compaction', {}) || {}; const compactionEnabled = document.getElementById('adminCompactionEnabled'); if (compactionEnabled) { compactionEnabled.checked = !!compactionCfg.enabled; document.getElementById('compactionConfigFields').style.display = compactionCfg.enabled ? '' : 'none'; } const compThreshold = document.getElementById('adminCompactionThreshold'); if (compThreshold) compThreshold.value = String(compactionCfg.threshold || 70); const compCooldown = document.getElementById('adminCompactionCooldown'); if (compCooldown) compCooldown.value = String(compactionCfg.cooldown || 30); // Memory extraction config (v0.18.0) const memExtractionCfg = getSetting('memory_extraction', {}) || {}; const memEnabled = document.getElementById('adminMemoryExtractionEnabled'); if (memEnabled) { memEnabled.checked = !!memExtractionCfg.enabled; const memFields = document.getElementById('memoryExtractionConfigFields'); if (memFields) memFields.style.display = memExtractionCfg.enabled ? '' : 'none'; memEnabled.addEventListener('change', () => { if (memFields) memFields.style.display = memEnabled.checked ? '' : 'none'; }); } const memAutoApprove = document.getElementById('adminMemoryAutoApprove'); if (memAutoApprove) memAutoApprove.checked = !!memExtractionCfg.auto_approve; // Email / SMTP config (v0.20.0 Phase 3) const notifCfg = getSetting('notifications', {}) || {}; if (typeof NotifPrefs !== 'undefined') { NotifPrefs._loadAdminSmtp(notifCfg); NotifPrefs._initAdminSmtp(); } if (vaultEl) { try { const vault = await API.adminGetVaultStatus(); const keyBadge = vault.encryption_key_set ? 'Active' : 'Not Set'; vaultEl.innerHTML = `
Encryption ${keyBadge}
Encrypted Keys ${vault.encrypted_keys}
Vault Users ${vault.vault_users}
`; } catch (e) { vaultEl.innerHTML = 'Could not load vault status'; } } } catch (e) { console.debug('Failed to load admin settings:', e); } }, updateBannerPreview() { const prev = document.getElementById('bannerPreview'); if (!prev) return; const text = document.getElementById('adminBannerText').value || 'PREVIEW'; const bg = document.getElementById('adminBannerBg').value; const fg = document.getElementById('adminBannerFg').value; prev.textContent = text; prev.style.background = bg; prev.style.color = fg; }, // ── Admin Channels (v0.23.2) ────────────── async loadAdminChannels() { const list = document.getElementById('adminArchivedList'); const countEl = document.getElementById('adminArchivedCount'); const modeEl = document.getElementById('adminRetentionMode'); if (!list) return; try { const resp = await API._get('/api/v1/admin/channels/archived'); const channels = resp.channels || []; if (countEl) countEl.textContent = channels.length; if (modeEl) modeEl.textContent = resp.retention_mode || 'flexible'; if (channels.length === 0) { list.innerHTML = '
No archived channels
'; return; } let html = '' + '' + ''; for (const ch of channels) { const age = _relativeTime(ch.updated_at); html += ``; } html += '
TitleTypeOwnerMessagesArchived
${esc(ch.title || 'Untitled')} ${esc(ch.type)} ${esc(ch.owner_name)} ${ch.message_count} ${age}
'; list.innerHTML = html; } catch (e) { list.innerHTML = `
Failed to load: ${esc(e.message)}
`; } }, async _purgeChannel(channelId) { if (!await showConfirm('Permanently delete this archived channel and all its messages? This cannot be undone.')) return; try { await API._del(`/api/v1/admin/channels/${channelId}/purge`); UI.toast('Channel purged', 'success'); UI.loadAdminChannels(); // refresh list } catch (e) { UI.toast('Purge failed: ' + e.message, 'error'); } }, });