// ========================================== // Chat Switchboard – Admin Handlers // ========================================== // Admin actions: user management, roles, model visibility, // presets, team management, global providers. // ── Admin Actions ──────────────────────────── function _adminScroll() { return document.querySelector('#adminModal .modal-body'); } function _restoreScroll(el, pos) { if (el) requestAnimationFrame(() => { el.scrollTop = pos; }); } async function toggleUserActive(id, active) { try { const el = _adminScroll(), pos = el?.scrollTop || 0; await API.adminToggleActive(id, active); UI.toast(`User ${active ? 'enabled' : 'disabled'}`, 'success'); await UI.loadAdminUsers(); _restoreScroll(el, pos); } catch (e) { UI.toast(e.message, 'error'); } } async function showApproveForm(userId, username) { const form = document.getElementById(`approveForm-${userId}`); const teamsEl = document.getElementById(`approveTeams-${userId}`); if (!form || !teamsEl) return; // Load teams for checkboxes teamsEl.innerHTML = 'Loading teams...'; form.style.display = ''; try { const resp = await API.adminListTeams(); const teams = (resp.data || []).filter(t => t.is_active); if (teams.length === 0) { teamsEl.innerHTML = 'No teams — user will be activated without team assignment'; } else { teamsEl.innerHTML = '' + teams.map(t => ``).join(''); } } catch (e) { teamsEl.innerHTML = `Could not load teams`; } } function hideApproveForm(userId) { const form = document.getElementById(`approveForm-${userId}`); if (form) form.style.display = 'none'; } async function submitApproval(userId) { const form = document.getElementById(`approveForm-${userId}`); const teamIds = [...(form?.querySelectorAll('.approve-team-cb:checked') || [])].map(cb => cb.value); try { const el = _adminScroll(), pos = el?.scrollTop || 0; await API.adminToggleActive(userId, true, teamIds, 'member'); const msg = teamIds.length ? `User activated & assigned to ${teamIds.length} team(s)` : 'User activated'; UI.toast(msg, 'success'); await UI.loadAdminUsers(); _restoreScroll(el, pos); } catch (e) { UI.toast(e.message, 'error'); } } async function toggleUserRole(id, role) { try { const el = _adminScroll(), pos = el?.scrollTop || 0; await API.adminUpdateRole(id, role); UI.toast(`Role updated to ${role}`, 'success'); await UI.loadAdminUsers(); _restoreScroll(el, pos); } catch (e) { UI.toast(e.message, 'error'); } } async function deleteUser(id, username) { if (!confirm(`Delete user "${username}"? This cannot be undone.`)) return; try { await API.adminDeleteUser(id); UI.toast('User deleted', 'success'); await UI.loadAdminUsers(); } catch (e) { UI.toast(e.message, 'error'); } } async function createAdminUser() { try { const u = document.getElementById('adminNewUsername').value.trim(); const e = document.getElementById('adminNewEmail').value.trim(); const p = document.getElementById('adminNewPassword').value; const r = document.getElementById('adminNewRole').value; if (!u || !e || !p) { UI.toast('All fields required', 'warning'); return; } await API.adminCreateUser(u, e, p, r); document.getElementById('adminAddUserForm').style.display = 'none'; UI.toast('User created', 'success'); await UI.loadAdminUsers(); } catch (e) { UI.toast(e.message, 'error'); } } async function adminResetUserPassword(id, username) { const pw = prompt( `Reset password for "${username}"?\n\n` + `⚠️ WARNING: This will DESTROY the user's personal vault.\n` + `All personal API keys (BYOK) will be permanently deleted.\n` + `The user will need to re-add any personal provider keys.\n\n` + `Enter new password (min 8 chars):` ); if (!pw) return; if (pw.length < 8) { UI.toast('Password must be at least 8 characters', 'warning'); return; } if (!confirm(`Final confirmation: Reset password AND destroy personal vault for "${username}"?`)) return; try { await API.adminResetPassword(id, pw); UI.toast(`Password reset for ${username}. Vault destroyed.`, 'success'); } catch (e) { UI.toast(e.message, 'error'); } } async function deleteGlobalProvider(id) { if (!confirm('Delete this global provider?')) return; try { await API.adminDeleteGlobalConfig(id); UI.toast('Provider deleted', 'success'); await UI.loadAdminProviders(); } catch (e) { UI.toast(e.message, 'error'); } } var _editingProviderId = null; function editGlobalProvider(id) { const c = UI._providerCache?.[id]; if (!c) return; _editingProviderId = id; const form = document.getElementById('adminAddProviderForm'); form.style.display = ''; document.getElementById('adminProvName').value = c.name || ''; document.getElementById('adminProvType').value = c.provider || 'openai'; document.getElementById('adminProvEndpoint').value = c.endpoint || ''; document.getElementById('adminProvKey').value = ''; document.getElementById('adminProvKey').placeholder = c.has_key ? '(unchanged)' : 'sk-...'; document.getElementById('adminProvModel').value = c.model_default || ''; document.getElementById('adminProvPrivate').checked = !!c.is_private; document.getElementById('adminCreateProvBtn').textContent = 'Update'; } async function createGlobalProvider() { try { const name = document.getElementById('adminProvName').value.trim(); const prov = document.getElementById('adminProvType').value; const ep = document.getElementById('adminProvEndpoint').value.trim(); const key = document.getElementById('adminProvKey').value; const model = document.getElementById('adminProvModel').value.trim(); const isPrivate = document.getElementById('adminProvPrivate').checked; if (_editingProviderId) { // Update mode const updates = {}; if (name) updates.name = name; if (ep) updates.endpoint = ep; if (key) updates.api_key = key; updates.model_default = model; updates.is_private = isPrivate; await API.adminUpdateGlobalConfig(_editingProviderId, updates); UI.toast('Provider updated', 'success'); } else { // Create mode if (!name || !ep || !key) { UI.toast('Name, endpoint, and API key required', 'warning'); return; } await API.adminCreateGlobalConfig(name, prov, ep, key, model, isPrivate); UI.toast('Provider added', 'success'); } _editingProviderId = null; document.getElementById('adminAddProviderForm').style.display = 'none'; document.getElementById('adminCreateProvBtn').textContent = 'Save'; document.getElementById('adminProvKey').placeholder = 'sk-...'; await UI.loadAdminProviders(); } catch (e) { UI.toast(e.message, 'error'); } } async function fetchAdminModels() { const hint = document.getElementById('adminModelsHint'); hint.textContent = 'Fetching...'; try { const resp = await API.adminFetchModels(); const errs = resp.errors || []; if (errs.length > 0) { UI.toast('Fetch errors: ' + errs.join('; '), 'error'); } else { UI.toast(`Models synced — ${resp.added || 0} added, ${resp.updated || 0} updated`, 'success'); } hint.textContent = ''; await UI.loadAdminModels(); } catch (e) { UI.toast(e.message, 'error'); hint.textContent = 'Failed'; } } async function cycleModelVisibility(id, current) { // Cycle: disabled → enabled → team → disabled const next = current === 'disabled' ? 'enabled' : current === 'enabled' ? 'team' : 'disabled'; try { const el = _adminScroll(), pos = el?.scrollTop || 0; // Send both for backward compat with pre-0.8.3 backends await API.adminUpdateModel(id, { visibility: next, is_enabled: next === 'enabled' }); await UI.loadAdminModels(); _restoreScroll(el, pos); } catch (e) { UI.toast(e.message, 'error'); } } async function bulkSetVisibility(visibility) { const hint = document.getElementById('adminModelsHint'); const labels = { enabled: 'Enabling', disabled: 'Disabling', team: 'Setting team-only for' }; hint.textContent = `${labels[visibility] || 'Updating'} all...`; try { // Send both for backward compat with pre-0.8.3 backends const resp = await API.adminBulkUpdateModels(visibility); const doneLabels = { enabled: 'enabled', disabled: 'disabled', team: 'set to team-only' }; hint.textContent = `${resp.count || 'All'} models ${doneLabels[visibility]}`; setTimeout(() => { hint.textContent = ''; }, 3000); await UI.loadAdminModels(); } catch (e) { UI.toast(e.message, 'error'); hint.textContent = 'Failed'; } } // ── Admin Roles ──────────────────────────── async function adminRoleProviderChanged(role, slot) { const provId = document.getElementById(`role-${role}-${slot}-provider`)?.value; const modelSelect = document.getElementById(`role-${role}-${slot}-model`); if (!modelSelect) return; modelSelect.innerHTML = ''; if (!provId || !window._adminModelList) return; // Filter by provider AND model type matching the role const typeForRole = role === 'embedding' ? 'embedding' : 'chat'; const models = window._adminModelList.filter(m => m.provider_config_id === provId && (m.model_type || 'chat') === typeForRole ); models.forEach(m => { const opt = document.createElement('option'); opt.value = m.model_id; opt.textContent = m.display_name || m.model_id; modelSelect.appendChild(opt); }); } async function adminSaveRole(role) { const status = document.getElementById(`role-${role}-status`); try { const getBinding = (slot) => { const prov = document.getElementById(`role-${role}-${slot}-provider`)?.value; const model = document.getElementById(`role-${role}-${slot}-model`)?.value; return (prov && model) ? { provider_config_id: prov, model_id: model } : null; }; await API.adminUpdateRole(role, { primary: getBinding('primary'), fallback: getBinding('fallback') }); if (status) { status.textContent = '✓ Saved'; status.style.color = 'var(--success)'; } // Reload to reflect saved state in dropdowns setTimeout(() => UI.loadAdminRoles(), 500); } catch (e) { if (status) { status.textContent = '✗ ' + e.message; status.style.color = 'var(--error)'; } } } async function adminTestRole(role) { const status = document.getElementById(`role-${role}-status`); if (status) { status.textContent = 'Testing...'; status.style.color = 'var(--text-muted)'; } try { const result = await API.adminTestRole(role); if (status) { const fb = result.used_fallback ? ' (fallback)' : ''; status.textContent = `✓ ${result.model}${fb}`; status.style.color = 'var(--success)'; } } catch (e) { if (status) { status.textContent = '✗ ' + e.message; status.style.color = 'var(--error)'; } } } // ── User Model Preferences ────────────────── async function toggleUserModelVisibility(modelId, currentlyHidden) { try { await API.setModelPreference(modelId, !currentlyHidden); if (currentlyHidden) App.hiddenModels.delete(modelId); else App.hiddenModels.add(modelId); await UI.loadUserModels(); await fetchModels(); // refresh model selector } catch (e) { UI.toast(e.message, 'error'); } } async function bulkSetUserModelVisibility(show) { const models = App.models.filter(m => !m.isPreset); if (!models.length) return; const shouldHide = !show; // Only update models not already in the desired state const toUpdate = models .map(m => m.baseModelId || m.id) .filter(mid => App.hiddenModels.has(mid) !== shouldHide); if (!toUpdate.length) { UI.toast(show ? 'All already visible' : 'All already hidden'); return; } try { await API.bulkSetModelPreferences(toUpdate, shouldHide); toUpdate.forEach(mid => shouldHide ? App.hiddenModels.add(mid) : App.hiddenModels.delete(mid)); await UI.loadUserModels(); await fetchModels(); UI.toast(show ? `${toUpdate.length} model(s) shown` : `${toUpdate.length} model(s) hidden`); } catch (e) { UI.toast(e.message, 'error'); } } async function deleteUserPreset(id, name) { if (!confirm(`Delete preset "${name}"?`)) return; try { await API.deleteUserPreset(id); UI.toast('Preset deleted'); await UI.loadUserPresets(); await fetchModels(); } catch (e) { UI.toast(e.message, 'error'); } } // ── Admin Presets ──────────────────────────── var _adminPresetForm = null; function ensureAdminPresetForm() { if (_adminPresetForm) return _adminPresetForm; const container = document.getElementById('adminAddPresetForm'); if (!container) return null; _adminPresetForm = renderPresetForm(container, { prefix: 'adminPreset', showAvatar: true, showProviderConfig: true, onSubmit: (vals) => createAdminPreset(vals), onCancel: () => { container.style.display = 'none'; _editingPresetId = null; _adminPresetForm.setSubmitLabel('Create'); _adminPresetForm.clearForm(); } }); // Override avatar handlers for edit-mode (upload immediately for existing presets) const fileInput = document.getElementById('adminPreset_avatarFileInput'); if (fileInput) { // Remove default handler set by renderPresetForm, add edit-aware one const newInput = fileInput.cloneNode(true); fileInput.parentNode.replaceChild(newInput, fileInput); newInput.addEventListener('change', async function() { const file = this.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = async () => { if (_editingPresetId) { try { const data = await API.adminUploadPresetAvatar(_editingPresetId, reader.result); if (data.avatar) { _adminPresetForm.updateAvatarPreview(data.avatar); await UI.loadAdminPresets(true); await fetchModels(); UI.toast('Preset avatar updated'); } } catch (e) { UI.toast(e.message || 'Upload failed', 'error'); } } else { _adminPresetForm.updateAvatarPreview(reader.result); } }; reader.readAsDataURL(file); this.value = ''; }); document.getElementById('adminPreset_avatarUploadBtn')?.addEventListener('click', () => newInput.click()); } const removeBtn = document.getElementById('adminPreset_avatarRemoveBtn'); if (removeBtn) { const newBtn = removeBtn.cloneNode(true); removeBtn.parentNode.replaceChild(newBtn, removeBtn); newBtn.addEventListener('click', async () => { if (_editingPresetId) { try { await API.adminDeletePresetAvatar(_editingPresetId); _adminPresetForm.updateAvatarPreview(null); await UI.loadAdminPresets(true); await fetchModels(); UI.toast('Preset avatar removed'); } catch (e) { UI.toast(e.message || 'Remove failed', 'error'); } } else { _adminPresetForm.updateAvatarPreview(null); } }); } return _adminPresetForm; } var _editingPresetId = null; function editAdminPreset(id) { const p = UI._presetCache?.[id]; if (!p) return; _editingPresetId = id; ensureAdminPresetForm(); const form = _adminPresetForm; if (!form) return; document.getElementById('adminAddPresetForm').style.display = ''; form.setValues(p); form.setSubmitLabel('Update'); } async function createAdminPreset(vals) { if (!vals) { if (_adminPresetForm) vals = _adminPresetForm.getValues(); else return; } if (!vals.name || !vals.base_model_id) { UI.toast('Name and base model are required', 'warning'); return; } const preset = { name: vals.name, base_model_id: vals.base_model_id, description: vals.description || '', system_prompt: vals.system_prompt || '', }; if (vals.provider_config_id) preset.provider_config_id = vals.provider_config_id; if (vals.temperature != null) preset.temperature = vals.temperature; if (vals.max_tokens != null) preset.max_tokens = vals.max_tokens; try { let presetId = _editingPresetId; if (_editingPresetId) { await API.adminUpdatePreset(_editingPresetId, preset); UI.toast('Preset updated', 'success'); } else { const result = await API.adminCreatePreset(preset); presetId = result?.id || result?.preset?.id; UI.toast('Preset created', 'success'); } // Upload pending avatar for new presets if (!_editingPresetId && presetId && vals._pendingAvatar) { try { await API.adminUploadPresetAvatar(presetId, vals._pendingAvatar); } catch (e) { console.warn('Preset avatar upload failed:', e.message); } } _editingPresetId = null; document.getElementById('adminAddPresetForm').style.display = 'none'; if (_adminPresetForm) { _adminPresetForm.setSubmitLabel('Create'); _adminPresetForm.clearForm(); } await UI.loadAdminPresets(); await fetchModels(); } catch (e) { UI.toast(e.message, 'error'); } } async function toggleAdminPreset(id, active) { try { const el = _adminScroll(), pos = el?.scrollTop || 0; await API.adminUpdatePreset(id, { is_active: active }); await UI.loadAdminPresets(true); _restoreScroll(el, pos); await fetchModels(); } catch (e) { UI.toast(e.message, 'error'); } } async function deleteAdminPreset(id, name) { if (!confirm(`Delete preset "${name}"?`)) return; try { await API.adminDeletePreset(id); UI.toast('Preset deleted', 'success'); await UI.loadAdminPresets(); await fetchModels(); } catch (e) { UI.toast(e.message, 'error'); } } // ── Team Admin Functions ──────────────────── async function toggleTeamActive(id, active) { try { await API.adminUpdateTeam(id, { is_active: active }); UI.toast(active ? 'Team activated' : 'Team deactivated'); await UI.loadAdminTeams(true); } catch (e) { UI.toast(e.message, 'error'); } } async function deleteTeam(id, name) { if (!confirm(`Delete team "${name}"? All members will be removed.`)) return; try { await API.adminDeleteTeam(id); UI.toast('Team deleted'); await UI.loadAdminTeams(true); } catch (e) { UI.toast(e.message, 'error'); } } async function updateTeamMember(teamId, memberId, role) { try { await API.adminUpdateMember(teamId, memberId, role); UI.toast('Role updated'); } catch (e) { UI.toast(e.message, 'error'); await UI.loadTeamMembers(teamId); } } async function removeTeamMember(teamId, memberId, email) { if (!confirm(`Remove ${email} from team?`)) return; try { await API.adminRemoveMember(teamId, memberId); UI.toast('Member removed'); await UI.loadTeamMembers(teamId); } catch (e) { UI.toast(e.message, 'error'); } } // Settings-side team management (uses team admin API, not sys-admin) async function settingsUpdateTeamMember(teamId, memberId, role) { try { await API.teamUpdateMember(teamId, memberId, role); UI.toast('Role updated'); } catch (e) { UI.toast(e.message, 'error'); await UI.loadTeamManageMembers(teamId); } } async function settingsRemoveTeamMember(teamId, memberId, email) { if (!confirm(`Remove ${email} from team?`)) return; try { await API.teamRemoveMember(teamId, memberId); UI.toast('Member removed'); await UI.loadTeamManageMembers(teamId); } catch (e) { UI.toast(e.message, 'error'); } } async function settingsDeleteTeamPreset(teamId, presetId, name) { if (!confirm(`Delete team preset "${name}"?`)) return; try { await API.teamDeletePreset(teamId, presetId); UI.toast('Preset deleted'); await UI.loadTeamManagePresets(teamId); await fetchModels(); } catch (e) { UI.toast(e.message, 'error'); } } async function settingsToggleTeamProvider(teamId, providerId, currentlyActive) { try { await API.teamUpdateProvider(teamId, providerId, { is_active: !currentlyActive }); UI.toast(currentlyActive ? 'Provider deactivated' : 'Provider activated'); await UI.loadTeamManageProviders(teamId); await fetchModels(); } catch (e) { UI.toast(e.message, 'error'); } } async function settingsDeleteTeamProvider(teamId, providerId, name) { if (!confirm(`Delete team provider "${name}"? This will remove all models from this provider for your team.`)) return; try { await API.teamDeleteProvider(teamId, providerId); UI.toast('Provider deleted'); await UI.loadTeamManageProviders(teamId); await fetchModels(); } catch (e) { UI.toast(e.message, 'error'); } } function settingsEditTeamProvider(teamId, provId, name, endpoint, defaultModel, isPrivate) { UI._editingTeamProviderId = provId; document.getElementById('settingsTeamAddProvider').style.display = 'none'; const form = document.getElementById('settingsTeamEditProvider'); form.style.display = ''; document.getElementById('teamProviderEditName').value = name; document.getElementById('teamProviderEditEndpoint').value = endpoint; document.getElementById('teamProviderEditKey').value = ''; document.getElementById('teamProviderEditDefault').value = defaultModel; document.getElementById('teamProviderEditPrivate').checked = isPrivate; } // ── Admin Listeners (extracted from initListeners) ── function _initAdminListeners() { // Admin modal (elements may not exist for non-admin users) document.getElementById('adminCloseBtn')?.addEventListener('click', UI.closeAdmin); document.querySelectorAll('.admin-tab').forEach(tab => { tab.addEventListener('click', () => UI.switchAdminTab(tab.dataset.tab)); }); document.getElementById('adminSaveSettings')?.addEventListener('click', handleSaveAdminSettings); // Admin — users document.getElementById('adminAddUserBtn')?.addEventListener('click', () => { const f = document.getElementById('adminAddUserForm'); f.style.display = f.style.display === 'none' ? '' : 'none'; }); document.getElementById('adminCancelUserBtn')?.addEventListener('click', () => { document.getElementById('adminAddUserForm').style.display = 'none'; }); document.getElementById('adminCreateUserBtn')?.addEventListener('click', createAdminUser); // Admin — providers document.getElementById('adminAddProviderBtn')?.addEventListener('click', () => { _editingProviderId = null; document.getElementById('adminCreateProvBtn').textContent = 'Save'; document.getElementById('adminProvKey').placeholder = 'sk-...'; document.getElementById('adminProvName').value = ''; document.getElementById('adminProvEndpoint').value = ''; document.getElementById('adminProvKey').value = ''; document.getElementById('adminProvModel').value = ''; document.getElementById('adminProvPrivate').checked = false; const f = document.getElementById('adminAddProviderForm'); f.style.display = f.style.display === 'none' ? '' : 'none'; }); document.getElementById('adminCancelProvBtn')?.addEventListener('click', () => { document.getElementById('adminAddProviderForm').style.display = 'none'; _editingProviderId = null; document.getElementById('adminCreateProvBtn').textContent = 'Save'; document.getElementById('adminProvKey').placeholder = 'sk-...'; }); document.getElementById('adminCreateProvBtn')?.addEventListener('click', createGlobalProvider); // Admin — models document.getElementById('adminFetchModelsBtn')?.addEventListener('click', fetchAdminModels); // Admin — presets (shared form) document.getElementById('adminAddPresetBtn')?.addEventListener('click', () => { const form = ensureAdminPresetForm(); if (!form) return; _editingPresetId = null; form.setSubmitLabel('Create'); form.clearForm(); const f = document.getElementById('adminAddPresetForm'); f.style.display = f.style.display === 'none' ? '' : 'none'; }); // Admin — Teams document.getElementById('adminAddTeamBtn')?.addEventListener('click', () => { document.getElementById('adminAddTeamForm').style.display = ''; document.getElementById('adminTeamName').focus(); }); document.getElementById('adminCancelTeamBtn')?.addEventListener('click', () => { document.getElementById('adminAddTeamForm').style.display = 'none'; document.getElementById('adminTeamName').value = ''; document.getElementById('adminTeamDesc').value = ''; }); document.getElementById('adminCreateTeamBtn')?.addEventListener('click', async () => { const name = document.getElementById('adminTeamName').value.trim(); const desc = document.getElementById('adminTeamDesc').value.trim(); if (!name) return UI.toast('Team name required', 'error'); try { await API.adminCreateTeam(name, desc); document.getElementById('adminAddTeamForm').style.display = 'none'; document.getElementById('adminTeamName').value = ''; document.getElementById('adminTeamDesc').value = ''; UI.toast('Team created'); await UI.loadAdminTeams(true); } catch (e) { UI.toast(e.message, 'error'); } }); document.getElementById('adminTeamBackBtn')?.addEventListener('click', () => { document.getElementById('adminTeamDetail').style.display = 'none'; document.getElementById('adminTeamList').style.display = ''; UI.loadAdminTeams(true); }); document.getElementById('adminAddMemberBtn')?.addEventListener('click', async () => { document.getElementById('adminAddMemberForm').style.display = ''; await UI.loadMemberUserDropdown(UI._teamEditId); }); document.getElementById('adminTeamPrivatePolicy')?.addEventListener('change', async function() { try { await API.adminUpdateTeam(UI._teamEditId, { settings: JSON.stringify({ require_private_providers: this.checked }) }); UI.toast(this.checked ? 'Private providers required' : 'Private provider policy removed'); } catch (e) { UI.toast(e.message, 'error'); this.checked = !this.checked; } }); document.getElementById('adminTeamAllowProviders')?.addEventListener('change', async function() { try { await API.adminUpdateTeam(UI._teamEditId, { settings: JSON.stringify({ allow_team_providers: this.checked }) }); UI.toast(this.checked ? 'Team providers enabled' : 'Team providers disabled'); } catch (e) { UI.toast(e.message, 'error'); this.checked = !this.checked; } }); document.getElementById('adminCancelMemberBtn')?.addEventListener('click', () => { document.getElementById('adminAddMemberForm').style.display = 'none'; }); document.getElementById('adminAddMemberSubmit')?.addEventListener('click', async () => { const userId = document.getElementById('adminMemberUser').value; const role = document.getElementById('adminMemberRole').value; if (!userId) return UI.toast('Select a user', 'error'); try { await API.adminAddMember(UI._teamEditId, userId, role); document.getElementById('adminAddMemberForm').style.display = 'none'; UI.toast('Member added'); await UI.loadTeamMembers(UI._teamEditId); } catch (e) { UI.toast(e.message, 'error'); } }); }