// ========================================== // 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', 'presets', 'roles', 'knowledgeBases'], system: ['settings', 'storage', 'extensions'], monitoring: ['usage', 'audit', 'stats'], }; const ADMIN_LABELS = { users: 'Users', teams: 'Teams', groups: 'Groups', providers: 'Providers', models: 'Models', presets: 'Personas', roles: 'Roles', knowledgeBases: 'Knowledge', 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(), presets: () => UI.loadAdminPresets(), knowledgeBases: () => { if (typeof KnowledgeUI === 'undefined') { console.error('[Admin] KnowledgeUI not loaded — check js/knowledge-ui.js'); return; } KnowledgeUI.openAdminPanel(); }, settings: () => UI.loadAdminSettings(), storage: () => typeof loadAdminStorage === 'function' ? loadAdminStorage() : null, extensions: () => UI.loadAdminExtensions(), usage: () => UI.loadAdminUsage(), audit: () => UI.loadAuditLog(), stats: () => UI.loadAdminStats(), }; // 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 === 'presets') UI.loadTeamManagePresets(teamId); if (tab === 'usage') UI.loadTeamUsage(); if (tab === 'activity') UI.loadTeamAuditLog(1); 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 || []; el.innerHTML = users.map(u => { const teamBadges = (u.teams || []).map(t => `${esc(t.team_name)}` ).join(' '); const isPending = !u.is_active; return `
${esc(u.username)} ${u.role} ${isPending ? 'pending' : ''} ${teamBadges}
${esc(u.email)}
${isPending ? `` : `` }
`; }).join('') || '
No users
'; } 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' }; el.innerHTML = '
' + Object.entries(s).map(([k, v]) => `
${typeof v === 'number' ? v.toLocaleString() : esc(String(v))}
${labels[k] || k.replace(/_/g, ' ')}
`).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() { // Usage stats via primitive const totalsEl = document.getElementById('adminUsageTotals'); if (!totalsEl) return; // Wrap totals+results in a container for the primitive (first time only) 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 (admin-only, separate from usage primitive) 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; // store for edit access 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 `
${esc(ext.name)} ${esc(ext.ext_id)} ${statusBadge}${systemBadge}
${esc(ext.description || 'No description')} ${ext.author ? ` · by ${esc(ext.author)}` : ''} · v${esc(ext.version)} · ${ext.tier}
`; }).join(''); } 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 `
${esc(e.action)} ${esc(actor)} ${esc(e.resource_type)}${e.resource_id ? ':' + esc(e.resource_id).slice(0,8) : ''}
${meta ? `${meta}` : ''} ${timeStr} ${e.ip_address ? `${esc(e.ip_address)}` : ''}
`; }).join(''); } // 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) 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'); } formEl.style.display = 'none'; UI._adminProvForm.setCreateMode(); UI._adminProvList.refresh(); } catch (e) { UI.toast(e.message, 'error'); } }, onCancel: () => { formEl.style.display = 'none'; UI._adminProvForm.setCreateMode(); }, }); } // Initialize admin provider list primitive (once) if (!UI._adminProvList) { UI._adminProvList = renderProviderList(el, { apiFetch: () => API.adminListGlobalConfigs(), showEndpoint: true, showPrivate: true, emptyMsg: 'No global providers — add one above', onEdit: (prov) => { if (UI._adminProvForm) { UI._adminProvForm.setEditMode(prov.id, prov); formEl.style.display = ''; } }, onDelete: async (prov) => { if (!await showConfirm('Delete this global provider?')) return; try { await API.adminDeleteGlobalConfig(prov.id); UI.toast('Provider deleted', 'success'); UI._adminProvList.refresh(); } catch (e) { UI.toast(e.message, 'error'); } }, }); } await UI._adminProvList.refresh(quiet); }, 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 : []; el.innerHTML = arr.map(m => { const caps = m.capabilities || {}; const badges = renderCapBadges(caps, { compact: true }); // Support both old is_enabled (bool) and new visibility (string) 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 `
${esc(m.model_id || m.id)} ${badges} ${esc(m.provider_name || '')}
`; }).join('') || '
No models — add a provider first, then click Fetch Models
'; } catch (e) { el.innerHTML = `
${esc(e.message)}
`; } }, async loadAdminPresets(quiet) { const el = document.getElementById('adminPresetList'); if (!quiet) el.innerHTML = '
Loading...
'; try { const data = await API.adminListPresets(); const list = data.presets || []; // Ensure form is initialized for dropdown population ensureAdminPresetForm(); // Populate model dropdown from admin model list (all synced models) const modelSel = _adminPresetForm?.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 preset form:', e.message); } } // Populate config dropdown const cfgSel = _adminPresetForm?.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._presetCache = {}; el.innerHTML = list.map(p => { UI._presetCache[p.id] = p; const avatarEl = p.avatar ? `` : ''; // Scope badge with attribution 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)}` : ''; const status = p.is_active ? '' : 'inactive'; return `
${avatarEl}${esc(p.name)} ${scopeBadge} ${creator} ${status}
${esc(p.base_model_id)} · ${esc(p.provider_name || 'auto')}
${p.description ? `
${esc(p.description)}
` : ''}
`; }).join('') || '
No presets — create one to give users preconfigured model experiences
'; } 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 = ''; document.getElementById('adminAddTeamForm').style.display = 'none'; } try { const resp = await API.adminListTeams(); const teams = resp.data || []; el.innerHTML = teams.map(t => `
${esc(t.name)} ${t.is_active ? '' : 'inactive'}
${esc(t.description || 'No description')} · ${t.member_count} member${t.member_count !== 1 ? 's' : ''}
`).join('') || '
No teams yet — create one to organize users and set policies
'; } catch (e) { el.innerHTML = `
${esc(e.message)}
`; } }, async openTeamDetail(teamId, teamName) { this._teamEditId = teamId; document.getElementById('adminTeamList').style.display = 'none'; document.getElementById('adminAddTeamForm').style.display = 'none'; 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 || []; el.innerHTML = members.map(m => `
${esc(m.display_name || m.email)} ${m.role} ${m.user_role === 'admin' ? 'sys-admin' : ''}
${esc(m.email)}
`).join('') || '
No members — add users to this team
'; } 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 = ''; document.getElementById('adminAddGroupForm').style.display = 'none'; } try { const resp = await API.adminListGroups(); const groups = resp.data || []; el.innerHTML = groups.map(g => { const scopeBadge = g.scope === 'global' ? 'global' : `team`; return `
${esc(g.name)} ${scopeBadge}
${esc(g.description || 'No description')} · ${g.member_count} member${g.member_count !== 1 ? 's' : ''}
`; }).join('') || '
No groups yet — create one to control access to personas and knowledge bases
'; } catch (e) { el.innerHTML = `
${esc(e.message)}
`; } }, async openGroupDetail(groupId, groupName) { this._groupEditId = groupId; document.getElementById('adminGroupList').style.display = 'none'; document.getElementById('adminAddGroupForm').style.display = 'none'; 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 || []; el.innerHTML = members.map(m => `
${esc(m.display_name || m.username || m.email)}
${esc(m.email)} · added ${new Date(m.added_at).toLocaleDateString()}
`).join('') || '
No members — add users to this group
'; } 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'; }); } }, 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 presets / personas (policy: allow_user_personas) document.getElementById('adminUserPresetsToggle').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 = ''; App.models.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('adminBannerPosition').value = banner.position || 'both'; 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(); // Load banner presets (global_settings) const presets = getSetting('banner_presets', {}) || {}; const sel = document.getElementById('adminBannerPreset'); sel.innerHTML = ''; Object.entries(presets).forEach(([key, p]) => { sel.innerHTML += ``; }); UI._bannerPresets = presets; // 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); 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); } }, _bannerPresets: {}, 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; }, });