// ========================================== // Chat Switchboard – UI Admin // ========================================== // Extends UI with admin modal tabs: users, stats, roles, // usage, audit, providers, models, presets, teams, settings. Object.assign(UI, { // ── Admin Modal ────────────────────────── openAdmin() { openModal('adminModal'); UI.switchAdminTab('users'); }, closeAdmin() { closeModal('adminModal'); }, 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); }, async switchAdminTab(tab) { document.querySelectorAll('#adminModal .admin-tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tab)); document.querySelectorAll('.admin-tab-content').forEach(c => c.style.display = 'none'); const panel = document.getElementById(`admin${tab.charAt(0).toUpperCase() + tab.slice(1)}Tab`); if (panel) panel.style.display = ''; if (tab === 'users') await this.loadAdminUsers(); if (tab === 'stats') await this.loadAdminStats(); if (tab === 'audit') await this.loadAuditLog(); if (tab === 'providers') await this.loadAdminProviders(); if (tab === 'models') await this.loadAdminModels(); if (tab === 'presets') await this.loadAdminPresets(); if (tab === 'teams') await this.loadAdminTeams(); if (tab === 'settings') await this.loadAdminSettings(); if (tab === 'roles') await this.loadAdminRoles(); if (tab === 'usage') await this.loadAdminUsage(); if (tab === 'extensions') await this.loadAdminExtensions(); if (tab === 'storage' && typeof loadAdminStorage === 'function') await loadAdminStorage(); }, 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 = ``; } }, 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'; // 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'); 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; }, });