488 lines
20 KiB
JavaScript
488 lines
20 KiB
JavaScript
// ==========================================
|
|
// 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 = '<div class="admin-loading">Loading users...</div>';
|
|
|
|
try {
|
|
const data = await Backend.adminListUsers(1, 100);
|
|
const users = data.data || [];
|
|
|
|
if (users.length === 0) {
|
|
container.innerHTML = '<div class="admin-empty">No users found</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = users.map(u => {
|
|
const isSelf = u.id === Backend.user.id;
|
|
const actions = isSelf ? '<span class="admin-you">(you)</span>' : `
|
|
<select class="admin-role-select" onchange="changeUserRole('${u.id}', this.value)">
|
|
<option value="user" ${u.role === 'user' ? 'selected' : ''}>user</option>
|
|
<option value="admin" ${u.role === 'admin' ? 'selected' : ''}>admin</option>
|
|
<option value="moderator" ${u.role === 'moderator' ? 'selected' : ''}>moderator</option>
|
|
</select>
|
|
<button class="btn btn-small btn-secondary" onclick="openResetDialog('${u.id}', '${escapeHtml(u.username)}')" title="Reset password">🔑</button>
|
|
<button class="btn btn-small ${u.is_active ? 'btn-warning' : 'btn-success'}" onclick="toggleUserActive('${u.id}', ${!u.is_active})">${u.is_active ? 'Disable' : 'Enable'}</button>
|
|
<button class="btn btn-small btn-danger" onclick="deleteUser('${u.id}', '${escapeHtml(u.username)}')">Delete</button>
|
|
`;
|
|
|
|
return `
|
|
<div class="admin-user-row" data-id="${u.id}">
|
|
<div class="admin-user-info">
|
|
<span class="admin-user-name">${escapeHtml(u.username)}</span>
|
|
<span class="admin-user-email">${escapeHtml(u.email)}</span>
|
|
</div>
|
|
<div class="admin-user-meta">
|
|
<span class="admin-badge admin-badge-${u.role}">${u.role}</span>
|
|
<span class="admin-badge ${u.is_active ? 'admin-badge-active' : 'admin-badge-inactive'}">${u.is_active ? 'active' : 'disabled'}</span>
|
|
</div>
|
|
<div class="admin-user-actions">${actions}</div>
|
|
</div>`;
|
|
}).join('');
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="admin-error">Failed to load users: ' + e.message + '</div>';
|
|
}
|
|
}
|
|
|
|
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 = '<div class="admin-loading">Loading stats...</div>';
|
|
|
|
try {
|
|
const stats = await Backend.adminGetStats();
|
|
container.innerHTML = '<div class="admin-stats-grid">' +
|
|
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') +
|
|
'</div>';
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="admin-error">Failed to load stats: ' + e.message + '</div>';
|
|
}
|
|
}
|
|
|
|
function statCard(value, label) {
|
|
return '<div class="admin-stat-card"><div class="admin-stat-value">' +
|
|
(value || 0) + '</div><div class="admin-stat-label">' + label + '</div></div>';
|
|
}
|
|
|
|
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 = '<div class="admin-loading">Loading models...</div>';
|
|
|
|
try {
|
|
const data = await Backend.adminListModels();
|
|
const models = data.models || [];
|
|
|
|
if (models.length === 0) {
|
|
container.innerHTML = '<div class="admin-empty">No models configured. Add a provider first, then click "Fetch Models".</div>';
|
|
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 += '<div class="admin-model-group">';
|
|
html += '<div class="admin-model-group-header">' + escapeHtml(provider) + '</div>';
|
|
for (const m of provModels) {
|
|
const caps = m.capabilities || {};
|
|
const name = m.display_name || m.model_id;
|
|
html += '<div class="admin-model-row">';
|
|
html += ' <div class="admin-model-toggle">';
|
|
html += ' <label class="toggle-label-compact">';
|
|
html += ' <input type="checkbox" ' + (m.is_enabled ? 'checked' : '') +
|
|
' onchange="toggleModelEnabled(\'' + m.id + '\', this.checked)">';
|
|
html += ' </label>';
|
|
html += ' </div>';
|
|
html += ' <div class="admin-model-info">';
|
|
html += ' <span class="admin-model-name">' + escapeHtml(name) + '</span>';
|
|
html += ' <span class="admin-model-id">' + escapeHtml(m.model_id) + '</span>';
|
|
html += ' </div>';
|
|
html += ' <div class="admin-model-caps">';
|
|
for (const cap of CAPS) {
|
|
const active = caps[cap] === true;
|
|
html += ' <button class="cap-badge ' + (active ? 'cap-active' : 'cap-inactive') + '"' +
|
|
' onclick="toggleModelCap(\'' + m.id + '\', \'' + cap + '\', ' + !active + ', \'' + escapeHtml(JSON.stringify(caps)) + '\')"' +
|
|
' title="' + cap + '">' + cap + '</button>';
|
|
}
|
|
html += ' </div>';
|
|
html += ' <div class="admin-model-actions">';
|
|
html += ' <button class="btn btn-small btn-danger" onclick="deleteAdminModel(\'' + m.id + '\', \'' + escapeHtml(m.model_id) + '\')">✕</button>';
|
|
html += ' </div>';
|
|
html += '</div>';
|
|
}
|
|
html += '</div>';
|
|
}
|
|
|
|
container.innerHTML = html;
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="admin-error">Failed to load models: ' + e.message + '</div>';
|
|
}
|
|
}
|
|
|
|
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 = '<div class="admin-loading">Loading...</div>';
|
|
|
|
try {
|
|
const data = await Backend.adminListGlobalConfigs();
|
|
const configs = data.configs || [];
|
|
|
|
if (configs.length === 0) {
|
|
container.innerHTML = '<div class="provider-empty">No global providers. Users must add their own.</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = configs.map(c => `
|
|
<div class="provider-row">
|
|
<div class="provider-info">
|
|
<span class="provider-name">${escapeHtml(c.name)}</span>
|
|
<span class="provider-meta">${c.provider} · ${c.model_default || 'no default'}</span>
|
|
</div>
|
|
<div class="provider-actions">
|
|
<span class="admin-badge admin-badge-active">${c.has_key ? '🔑' : '⚠️'}</span>
|
|
<button class="btn btn-small btn-danger" onclick="deleteAdminProvider('${c.id}', '${escapeHtml(c.name)}')">Remove</button>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="admin-error">Failed to load providers: ' + e.message + '</div>';
|
|
}
|
|
}
|
|
|
|
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');
|
|
}
|
|
}
|