// ==========================================
// Admin Panel
// ==========================================
let _adminListenersInit = false;
function initAdmin() {
if (Backend.user && Backend.user.role === 'admin') {
document.getElementById('adminBtn').style.display = '';
} else {
document.getElementById('adminBtn').style.display = 'none';
}
}
function openAdmin() {
document.getElementById('adminModal').classList.add('active');
initAdminListeners();
switchAdminTab('users');
loadAdminUsers();
}
function closeAdmin() {
document.getElementById('adminModal').classList.remove('active');
}
function initAdminListeners() {
if (_adminListenersInit) return;
_adminListenersInit = true;
document.getElementById('adminShowAddUser').addEventListener('click', () => {
document.getElementById('adminAddUserForm').style.display = '';
document.getElementById('adminShowAddUser').style.display = 'none';
});
document.getElementById('adminCancelAddUser').addEventListener('click', () => {
document.getElementById('adminAddUserForm').style.display = 'none';
document.getElementById('adminShowAddUser').style.display = '';
clearAddUserForm();
});
document.getElementById('adminCreateUserBtn').addEventListener('click', handleCreateUser);
document.getElementById('adminResetCancelBtn').addEventListener('click', closeResetDialog);
document.getElementById('adminResetConfirmBtn').addEventListener('click', handleResetPassword);
// Admin providers
document.getElementById('adminProviderShowAddBtn').addEventListener('click', () => {
document.getElementById('adminProviderAddForm').style.display = '';
document.getElementById('adminProviderShowAddBtn').style.display = 'none';
});
document.getElementById('adminProviderCancelBtn').addEventListener('click', () => {
document.getElementById('adminProviderAddForm').style.display = 'none';
document.getElementById('adminProviderShowAddBtn').style.display = '';
clearAdminProviderForm();
});
document.getElementById('adminProviderCreateBtn').addEventListener('click', handleCreateAdminProvider);
// Admin models
document.getElementById('adminFetchModelsBtn').addEventListener('click', handleAdminFetchModels);
document.getElementById('adminResetNewPw').addEventListener('keydown', function(e) {
if (e.key === 'Enter') handleResetPassword();
});
}
function switchAdminTab(tab) {
document.querySelectorAll('.admin-tab').forEach(t => {
t.classList.toggle('active', t.dataset.tab === tab);
});
document.getElementById('adminUsersTab').style.display = tab === 'users' ? '' : 'none';
document.getElementById('adminModelsTab').style.display = tab === 'models' ? '' : 'none';
document.getElementById('adminProvidersTab').style.display = tab === 'providers' ? '' : 'none';
document.getElementById('adminSettingsTab').style.display = tab === 'settings' ? '' : 'none';
document.getElementById('adminStatsTab').style.display = tab === 'stats' ? '' : 'none';
document.getElementById('adminResetDialog').style.display = 'none';
if (tab === 'models') loadAdminModels();
if (tab === 'providers') loadAdminProviders();
if (tab === 'settings') loadAdminSettings();
if (tab === 'stats') loadAdminStats();
}
// ── Add User ────────────────────────────────
function clearAddUserForm() {
document.getElementById('adminNewUsername').value = '';
document.getElementById('adminNewEmail').value = '';
document.getElementById('adminNewPassword').value = '';
document.getElementById('adminNewRole').value = 'user';
}
async function handleCreateUser() {
const username = document.getElementById('adminNewUsername').value.trim();
const email = document.getElementById('adminNewEmail').value.trim();
const password = document.getElementById('adminNewPassword').value;
const role = document.getElementById('adminNewRole').value;
if (!username || !email || !password) {
showToast('⚠️ Fill in all fields', 'warning');
return;
}
if (password.length < 8) {
showToast('⚠️ Password must be at least 8 characters', 'warning');
return;
}
try {
await Backend.adminCreateUser(username, email, password, role);
showToast('✅ User "' + username + '" created', 'success');
clearAddUserForm();
document.getElementById('adminAddUserForm').style.display = 'none';
document.getElementById('adminShowAddUser').style.display = '';
loadAdminUsers();
} catch (e) {
showToast('❌ ' + e.message, 'error');
}
}
// ── Users List ──────────────────────────────
async function loadAdminUsers() {
const container = document.getElementById('adminUserList');
container.innerHTML = '
Loading users...
';
try {
const data = await Backend.adminListUsers(1, 100);
const users = data.data || [];
if (users.length === 0) {
container.innerHTML = 'No users found
';
return;
}
container.innerHTML = users.map(u => {
const isSelf = u.id === Backend.user.id;
const actions = isSelf ? '(you) ' : `
user
admin
moderator
🔑
${u.is_active ? 'Disable' : 'Enable'}
Delete
`;
return `
${escapeHtml(u.username)}
${escapeHtml(u.email)}
${u.role}
${u.is_active ? 'active' : 'disabled'}
${actions}
`;
}).join('');
} catch (e) {
container.innerHTML = 'Failed to load users: ' + e.message + '
';
}
}
async function changeUserRole(userId, role) {
try {
await Backend.adminUpdateUserRole(userId, role);
showToast('✅ Role updated to ' + role, 'success');
} catch (e) {
showToast('❌ ' + e.message, 'error');
loadAdminUsers();
}
}
async function toggleUserActive(userId, isActive) {
try {
await Backend.adminToggleUserActive(userId, isActive);
showToast('✅ User ' + (isActive ? 'enabled' : 'disabled'), 'success');
loadAdminUsers();
} catch (e) {
showToast('❌ ' + e.message, 'error');
}
}
async function deleteUser(userId, username) {
if (!confirm('Delete user "' + username + '"? This removes all their chats and data.')) return;
try {
await Backend.adminDeleteUser(userId);
showToast('✅ User deleted', 'success');
loadAdminUsers();
} catch (e) {
showToast('❌ ' + e.message, 'error');
}
}
// ── Reset Password ──────────────────────────
let _resetUserId = null;
function openResetDialog(userId, username) {
_resetUserId = userId;
document.getElementById('adminResetTarget').textContent = 'Set new password for: ' + username;
document.getElementById('adminResetNewPw').value = '';
document.getElementById('adminResetDialog').style.display = '';
document.getElementById('adminResetNewPw').focus();
}
function closeResetDialog() {
_resetUserId = null;
document.getElementById('adminResetDialog').style.display = 'none';
}
async function handleResetPassword() {
if (!_resetUserId) return;
const pw = document.getElementById('adminResetNewPw').value;
if (!pw || pw.length < 8) {
showToast('⚠️ Password must be at least 8 characters', 'warning');
return;
}
try {
await Backend.adminResetPassword(_resetUserId, pw);
showToast('✅ Password reset', 'success');
closeResetDialog();
} catch (e) {
showToast('❌ ' + e.message, 'error');
}
}
// ── Settings ────────────────────────────────
async function loadAdminSettings() {
try {
const data = await Backend.adminListSettings();
const settings = data.settings || [];
for (const s of settings) {
if (s.key === 'registration') {
document.getElementById('adminRegToggle').checked = s.value.enabled !== false;
}
if (s.key === 'site') {
document.getElementById('adminSiteName').value = s.value.name || 'Chat Switchboard';
document.getElementById('adminTagline').value = s.value.tagline || 'Multi-Model AI Chat';
}
}
} catch (e) {
showToast('❌ Failed to load settings: ' + e.message, 'error');
}
}
async function saveAdminSettings() {
try {
await Backend.adminUpdateSetting('registration', {
enabled: document.getElementById('adminRegToggle').checked
});
await Backend.adminUpdateSetting('site', {
name: document.getElementById('adminSiteName').value.trim(),
tagline: document.getElementById('adminTagline').value.trim()
});
showToast('✅ Admin settings saved', 'success');
} catch (e) {
showToast('❌ ' + e.message, 'error');
}
}
// ── Stats ───────────────────────────────────
async function loadAdminStats() {
const container = document.getElementById('adminStats');
container.innerHTML = 'Loading stats...
';
try {
const stats = await Backend.adminGetStats();
container.innerHTML = '' +
statCard(stats.total_users, 'Total Users') +
statCard(stats.active_users, 'Active Users') +
statCard(stats.total_chats, 'Chats') +
statCard(stats.total_messages, 'Messages') +
statCard(stats.api_configs, 'API Configs') +
'
';
} catch (e) {
container.innerHTML = 'Failed to load stats: ' + e.message + '
';
}
}
function statCard(value, label) {
return '' +
(value || 0) + '
' + label + '
';
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
// ── Admin Models ────────────────────────────
const CAPS = ['tool', 'thinking', 'vision', 'code'];
async function loadAdminModels() {
const container = document.getElementById('adminModelList');
container.innerHTML = 'Loading models...
';
try {
const data = await Backend.adminListModels();
const models = data.models || [];
if (models.length === 0) {
container.innerHTML = 'No models configured. Add a provider first, then click "Fetch Models".
';
return;
}
// Group by provider
const grouped = {};
for (const m of models) {
const key = m.provider_name || 'Unknown';
if (!grouped[key]) grouped[key] = [];
grouped[key].push(m);
}
let html = '';
for (const [provider, provModels] of Object.entries(grouped)) {
html += '';
html += '';
for (const m of provModels) {
const caps = m.capabilities || {};
const name = m.display_name || m.model_id;
html += '
';
html += '
';
html += ' ';
html += ' ';
html += ' ';
html += '
';
html += '
';
html += ' ' + escapeHtml(name) + ' ';
html += ' ' + escapeHtml(m.model_id) + ' ';
html += '
';
html += '
';
for (const cap of CAPS) {
const active = caps[cap] === true;
html += ' ' + cap + ' ';
}
html += '
';
html += '
';
html += ' ✕ ';
html += '
';
html += '
';
}
html += '
';
}
container.innerHTML = html;
} catch (e) {
container.innerHTML = 'Failed to load models: ' + e.message + '
';
}
}
async function handleAdminFetchModels() {
const btn = document.getElementById('adminFetchModelsBtn');
btn.disabled = true;
btn.textContent = '⏳ Fetching...';
try {
const data = await Backend.adminFetchModels();
const added = data.total_added || 0;
const results = data.results || [];
const errors = results.filter(r => r.error);
if (errors.length > 0) {
showToast('⚠️ Fetched with ' + errors.length + ' error(s). ' + added + ' new models.', 'warning');
} else {
showToast('✅ Fetched ' + added + ' new models', 'success');
}
loadAdminModels();
} catch (e) {
showToast('❌ ' + e.message, 'error');
} finally {
btn.disabled = false;
btn.textContent = '🔄 Fetch Models';
}
}
async function toggleModelEnabled(modelId, enabled) {
try {
await Backend.adminUpdateModel(modelId, { is_enabled: enabled });
} catch (e) {
showToast('❌ ' + e.message, 'error');
loadAdminModels();
}
}
async function toggleModelCap(modelId, cap, value, capsJson) {
try {
const caps = JSON.parse(capsJson);
caps[cap] = value;
await Backend.adminUpdateModel(modelId, { capabilities: caps });
loadAdminModels();
} catch (e) {
showToast('❌ ' + e.message, 'error');
loadAdminModels();
}
}
async function deleteAdminModel(modelId, modelName) {
if (!confirm('Remove model "' + modelName + '"? It can be re-fetched later.')) return;
try {
await Backend.adminDeleteModel(modelId);
showToast('✅ Model removed', 'success');
loadAdminModels();
} catch (e) {
showToast('❌ ' + e.message, 'error');
}
}
async function loadAdminProviders() {
const container = document.getElementById('adminProviderList');
container.innerHTML = 'Loading...
';
try {
const data = await Backend.adminListGlobalConfigs();
const configs = data.configs || [];
if (configs.length === 0) {
container.innerHTML = 'No global providers. Users must add their own.
';
return;
}
container.innerHTML = configs.map(c => `
${escapeHtml(c.name)}
${c.provider} · ${c.model_default || 'no default'}
${c.has_key ? '🔑' : '⚠️'}
Remove
`).join('');
} catch (e) {
container.innerHTML = 'Failed to load providers: ' + e.message + '
';
}
}
function clearAdminProviderForm() {
document.getElementById('adminProviderName').value = '';
document.getElementById('adminProviderType').value = 'openai';
document.getElementById('adminProviderEndpoint').value = '';
document.getElementById('adminProviderApiKey').value = '';
document.getElementById('adminProviderDefaultModel').value = '';
}
async function handleCreateAdminProvider() {
const name = document.getElementById('adminProviderName').value.trim();
const provider = document.getElementById('adminProviderType').value;
const endpoint = document.getElementById('adminProviderEndpoint').value.trim();
const apiKey = document.getElementById('adminProviderApiKey').value.trim();
const modelDefault = document.getElementById('adminProviderDefaultModel').value.trim();
if (!name || !endpoint || !apiKey) {
showToast('⚠️ Name, endpoint, and API key are required', 'warning');
return;
}
try {
await Backend.adminCreateGlobalConfig(name, provider, endpoint, apiKey, modelDefault || null);
showToast('✅ Global provider added', 'success');
clearAdminProviderForm();
document.getElementById('adminProviderAddForm').style.display = 'none';
document.getElementById('adminProviderShowAddBtn').style.display = '';
loadAdminProviders();
} catch (e) {
showToast('❌ ' + e.message, 'error');
}
}
async function deleteAdminProvider(configId, name) {
if (!confirm('Remove global provider "' + name + '"?')) return;
try {
await Backend.adminDeleteGlobalConfig(configId);
showToast('✅ Global provider removed', 'success');
loadAdminProviders();
} catch (e) {
showToast('❌ ' + e.message, 'error');
}
}