// ============================================================ // ADMIN.JS — Admin panel: users, settings, stats // ============================================================ (function() { var loaded = false; // Load admin panel when admin tab is activated document.addEventListener('tabChanged', function(e) { if (e.detail && e.detail.tab === 'admin') { if (!loaded) { loadAdmin(); loaded = true; } } }); document.addEventListener('click', function(e) { // Refresh button if (e.target.closest('#btn-refresh-users')) { loadUsers(); } // Toggle registration if (e.target.closest('#btn-toggle-reg')) { toggleRegistration(); } }); function loadAdmin() { loadSettings(); loadUsers(); } // ---- SETTINGS + STATS ---- function loadSettings() { fetch('/api/admin/settings', { headers: getAuthHeaders() }) .then(function(r) { return r.json(); }) .then(function(data) { if (!data.success) return; var stats = data.stats || {}; var settings = data.settings || {}; var el = document.getElementById('stat-users'); if (el) el.textContent = stats.totalUsers !== undefined ? stats.totalUsers : '—'; el = document.getElementById('stat-api-total'); if (el) el.textContent = stats.totalApiCalls !== undefined ? stats.totalApiCalls : '—'; el = document.getElementById('stat-api-today'); if (el) el.textContent = stats.todayApiCalls !== undefined ? stats.todayApiCalls : '—'; updateRegStatus(settings.registrationEnabled !== false); }) .catch(function(err) { console.error('[Admin] Settings load failed:', err); }); } function updateRegStatus(enabled) { var text = document.getElementById('reg-status-text'); var btn = document.getElementById('btn-toggle-reg'); if (text) { text.innerHTML = 'Registration is currently ' + (enabled ? '✅ Enabled' : '❌ Disabled') + ''; } if (btn) btn.textContent = enabled ? 'Disable Registration' : 'Enable Registration'; window._adminRegEnabled = enabled; } function toggleRegistration() { var newVal = !window._adminRegEnabled; fetch('/api/admin/settings/registration', { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ enabled: newVal }) }) .then(function(r) { return r.json(); }) .then(function(data) { if (data.success) { updateRegStatus(newVal); showToast('Registration ' + (newVal ? 'enabled' : 'disabled'), 'success'); } else { showToast(data.error || 'Failed', 'error'); } }) .catch(function() { showToast('Request failed', 'error'); }); } // ---- USERS ---- function loadUsers() { var tbody = document.getElementById('admin-users-body'); if (!tbody) return; tbody.innerHTML = 'Loading...'; fetch('/api/admin/users', { headers: getAuthHeaders() }) .then(function(r) { return r.json(); }) .then(function(data) { if (!data.success) { tbody.innerHTML = 'Failed to load users'; return; } renderUsers(data.users || []); }) .catch(function() { tbody.innerHTML = 'Request failed'; }); } function renderUsers(users) { var tbody = document.getElementById('admin-users-body'); if (!tbody) return; var currentUser = JSON.parse(localStorage.getItem('ped_scribe_user') || '{}'); if (users.length === 0) { tbody.innerHTML = 'No users found'; return; } tbody.innerHTML = users.map(function(u) { var isSelf = currentUser.id && u.id === currentUser.id; var roleColor = u.role === 'admin' ? 'var(--blue)' : u.role === 'moderator' ? 'var(--purple)' : 'var(--g500)'; var statusColor = u.disabled ? 'var(--red)' : (u.email_verified ? 'var(--green)' : 'var(--amber)'); var statusText = u.disabled ? 'Disabled' : (u.email_verified ? 'Active' : 'Unverified'); var joined = u.created_at ? new Date(u.created_at).toLocaleDateString() : '—'; var actions = []; if (!isSelf) { if (!u.email_verified) { actions.push(''); } if (u.disabled) { actions.push(''); } else { actions.push(''); } if (u.role === 'admin') { actions.push(''); } else { actions.push(''); } if (u.role !== 'moderator') { actions.push(''); } if (u.role === 'moderator') { actions.push(''); } actions.push(''); actions.push(''); } else { actions.push('(you)'); } return '' + '
' + esc(u.name) + '
' + esc(u.email) + '
' + '' + (u.role || 'user') + '' + '' + statusText + '' + '' + joined + '' + '' + actions.join(' ') + '' + ''; }).join(''); // Bind action buttons tbody.querySelectorAll('.admin-action').forEach(function(btn) { btn.addEventListener('click', function() { handleUserAction(btn.dataset.action, btn.dataset.id, btn.dataset.email, btn.dataset.name); }); }); } function handleUserAction(action, userId, email, name) { switch (action) { case 'verify': if (!confirm('Verify email for ' + email + '?')) return; adminPost('/api/admin/users/' + userId + '/verify', {}, function() { showToast(email + ' verified', 'success'); loadUsers(); }); break; case 'disable': if (!confirm('Disable account for ' + email + '?')) return; adminPost('/api/admin/users/' + userId + '/disable', {}, function() { showToast(email + ' disabled', 'info'); loadUsers(); }); break; case 'enable': adminPost('/api/admin/users/' + userId + '/enable', {}, function() { showToast(email + ' enabled', 'success'); loadUsers(); }); break; case 'promote': if (!confirm('Grant admin role to ' + email + '?')) return; adminPost('/api/admin/users/' + userId + '/role', { role: 'admin' }, function() { showToast(email + ' is now admin', 'success'); loadUsers(); }); break; case 'demote': if (!confirm('Remove admin role from ' + email + '?')) return; adminPost('/api/admin/users/' + userId + '/role', { role: 'user' }, function() { showToast(email + ' demoted to user', 'info'); loadUsers(); }); break; case 'set-moderator': if (!confirm('Set ' + email + ' as moderator? They will be able to manage Learning Hub content.')) return; adminPost('/api/admin/users/' + userId + '/role', { role: 'moderator' }, function() { showToast(email + ' is now a moderator', 'success'); loadUsers(); }); break; case 'set-user': if (!confirm('Set ' + email + ' to full user role?')) return; adminPost('/api/admin/users/' + userId + '/role', { role: 'user' }, function() { showToast(email + ' is now a user', 'success'); loadUsers(); }); break; case 'reset-pw': var newPw = prompt('New password for ' + email + ' (8+ characters):'); if (!newPw) return; if (newPw.length < 8) { showToast('Password must be 8+ characters', 'error'); return; } adminPost('/api/admin/users/' + userId + '/reset-password', { newPassword: newPw }, function() { showToast('Password reset for ' + email, 'success'); }); break; case 'delete': if (!confirm('Permanently delete user ' + (name || email) + '?\n\nThis cannot be undone.')) return; fetch('/api/admin/users/' + userId, { method: 'DELETE', headers: getAuthHeaders() }) .then(function(r) { return r.json(); }) .then(function(data) { if (data.success) { showToast(email + ' deleted', 'info'); loadUsers(); } else showToast(data.error || 'Delete failed', 'error'); }) .catch(function() { showToast('Request failed', 'error'); }); break; } } function adminPost(url, body, onSuccess) { fetch(url, { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify(body) }) .then(function(r) { return r.json(); }) .then(function(data) { if (data.success) { onSuccess(data); } else showToast(data.error || 'Request failed', 'error'); }) .catch(function() { showToast('Request failed', 'error'); }); } function esc(str) { if (!str) return ''; return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } })(); // ============================================================ // ADMIN CMS — Announcements, Feature Flags, Email, AI Prompts // ============================================================ (function() { var cmsLoaded = false; // Load CMS when admin tab is opened (via tabChanged event or click) document.addEventListener('tabChanged', function(e) { if (e.detail && e.detail.tab === 'admin') { if (!cmsLoaded) { loadCms(); cmsLoaded = true; } } }); document.addEventListener('click', function(e) { // Save buttons if (e.target.closest('#btn-save-announcement')) saveAnnouncement(); if (e.target.closest('#btn-save-flags')) saveFlags(); if (e.target.closest('#btn-save-email')) saveEmail(); if (e.target.closest('#btn-test-email')) sendTestEmail(); if (e.target.closest('#btn-save-prompt')) savePrompt(); if (e.target.closest('#btn-reset-prompt')) resetPrompt(); if (e.target.closest('#btn-save-smtp')) saveSmtp(); if (e.target.closest('#btn-clear-smtp')) clearSmtp(); if (e.target.closest('#btn-save-auto-delete')) saveAutoDelete(); if (e.target.closest('#btn-reset-all-defaults')) resetAllDefaults(); }); // When email template selector changes, repopulate fields document.addEventListener('change', function(e) { if (e.target.id === 'cms-email-template') loadEmailFields(e.target.value); if (e.target.id === 'cms-prompt-select') loadPromptText(e.target.value); }); // ---- LOAD ALL CONFIG ---- function loadCms() { fetch('/api/admin/config', { headers: getAuthHeaders() }) .then(function(r) { return r.json(); }) .then(function(data) { if (!data.success) return; var cfg = {}; (data.config || []).forEach(function(row) { cfg[row.key] = row.value; }); // Announcement var annEnabled = document.getElementById('cms-ann-enabled'); var annType = document.getElementById('cms-ann-type'); var annText = document.getElementById('cms-ann-text'); if (annEnabled) annEnabled.value = cfg['announcement.enabled'] || 'false'; if (annType) annType.value = cfg['announcement.type'] || 'info'; if (annText) annText.value = cfg['announcement.text'] || ''; // Feature flags var flagReadAloud = document.getElementById('cms-flag-read-aloud'); var flagNextcloud = document.getElementById('cms-flag-nextcloud'); if (flagReadAloud) flagReadAloud.value = cfg['feature.read_aloud'] !== undefined ? cfg['feature.read_aloud'] : 'true'; if (flagNextcloud) flagNextcloud.value = cfg['feature.nextcloud'] !== undefined ? cfg['feature.nextcloud'] : 'true'; // Auto-delete setting var autoDeleteDays = cfg['site.auto_delete_days'] || '7'; var el = document.getElementById('cms-auto-delete-days'); if (el) el.value = autoDeleteDays; el = document.getElementById('admin-auto-delete-days'); if (el) el.textContent = autoDeleteDays; // Email — load verify template by default window._adminEmailConfig = cfg; loadEmailFields('verify'); }) .catch(function(err) { console.error('[AdminCMS] Config load failed:', err); }); loadPromptList(); loadSmtp(); } // ---- ANNOUNCEMENT ---- function saveAnnouncement() { var enabled = document.getElementById('cms-ann-enabled').value; var type = document.getElementById('cms-ann-type').value; var text = document.getElementById('cms-ann-text').value; Promise.all([ putConfig('announcement.enabled', enabled), putConfig('announcement.type', type), putConfig('announcement.text', text) ]).then(function() { showToast('Announcement saved', 'success'); if (typeof loadAnnouncement === 'function') loadAnnouncement(); }).catch(function() { showToast('Save failed', 'error'); }); } // ---- FEATURE FLAGS ---- function saveFlags() { var readAloud = document.getElementById('cms-flag-read-aloud').value; var nextcloud = document.getElementById('cms-flag-nextcloud').value; Promise.all([ putConfig('feature.read_aloud', readAloud), putConfig('feature.nextcloud', nextcloud) ]).then(function() { showToast('Feature flags saved', 'success'); }).catch(function() { showToast('Save failed', 'error'); }); } // ---- EMAIL TEMPLATES ---- function loadEmailFields(template) { var cfg = window._adminEmailConfig || {}; var subject = document.getElementById('cms-email-subject'); var body = document.getElementById('cms-email-body'); if (subject) subject.value = cfg['email.' + template + '.subject'] || ''; if (body) body.value = cfg['email.' + template + '.body'] || ''; } function saveEmail() { var template = document.getElementById('cms-email-template').value; var subject = document.getElementById('cms-email-subject').value; var body = document.getElementById('cms-email-body').value; Promise.all([ putConfig('email.' + template + '.subject', subject), putConfig('email.' + template + '.body', body) ]).then(function() { // Update local cache if (!window._adminEmailConfig) window._adminEmailConfig = {}; window._adminEmailConfig['email.' + template + '.subject'] = subject; window._adminEmailConfig['email.' + template + '.body'] = body; showToast('Email template saved', 'success'); }).catch(function() { showToast('Save failed', 'error'); }); } function sendTestEmail() { var template = document.getElementById('cms-email-template').value; var to = prompt('Send test email to (enter email address):'); if (!to || !to.trim()) return; fetch('/api/admin/config/test-email', { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ to: to.trim(), template: template }) }) .then(function(r) { return r.json(); }) .then(function(data) { if (data.success) showToast('Test email sent to ' + to, 'success'); else showToast(data.error || 'Send failed', 'error'); }) .catch(function() { showToast('Request failed', 'error'); }); } // ---- AI PROMPTS ---- function loadPromptList() { fetch('/api/admin/config/prompts', { headers: getAuthHeaders() }) .then(function(r) { return r.json(); }) .then(function(data) { if (!data.success) return; var sel = document.getElementById('cms-prompt-select'); if (!sel) return; sel.innerHTML = ''; (data.prompts || []).forEach(function(p) { var opt = document.createElement('option'); opt.value = p.key; opt.textContent = p.key; sel.appendChild(opt); }); window._adminPrompts = {}; (data.prompts || []).forEach(function(p) { window._adminPrompts[p.key] = p.value; }); if (data.prompts && data.prompts.length > 0) { loadPromptText(data.prompts[0].key); } }) .catch(function(err) { console.error('[AdminCMS] Prompts load failed:', err); }); } function loadPromptText(key) { var textarea = document.getElementById('cms-prompt-text'); if (!textarea) return; textarea.value = (window._adminPrompts && window._adminPrompts[key]) || ''; } function savePrompt() { var sel = document.getElementById('cms-prompt-select'); var text = document.getElementById('cms-prompt-text'); if (!sel || !text) return; var key = sel.value; var value = text.value; if (!key) return; putConfig('prompt.' + key, value) .then(function() { if (!window._adminPrompts) window._adminPrompts = {}; window._adminPrompts[key] = value; showToast('Prompt saved', 'success'); }) .catch(function() { showToast('Save failed', 'error'); }); } function resetPrompt() { var sel = document.getElementById('cms-prompt-select'); if (!sel || !sel.value) return; var key = sel.value; if (!confirm('Reset "' + key + '" to the hardcoded default? This cannot be undone.')) return; fetch('/api/admin/config/prompts/' + key + '/reset', { method: 'POST', headers: getAuthHeaders() }) .then(function(r) { return r.json(); }) .then(function(data) { if (data.success) { if (!window._adminPrompts) window._adminPrompts = {}; window._adminPrompts[key] = data.value; var textarea = document.getElementById('cms-prompt-text'); if (textarea) textarea.value = data.value; showToast('Prompt reset to default', 'success'); } else { showToast(data.error || 'Reset failed', 'error'); } }) .catch(function() { showToast('Request failed', 'error'); }); } // ---- SMTP ---- function loadSmtp() { fetch('/api/admin/config/smtp/status', { headers: getAuthHeaders() }) .then(function(r) { return r.json(); }) .then(function(data) { if (!data.success) return; var badge = document.getElementById('smtp-source-badge'); if (badge) { if (data.source === 'env') badge.textContent = 'Configured via env'; else if (data.source === 'database') badge.textContent = 'Configured in DB'; else { badge.textContent = 'Not configured'; badge.style.background = 'var(--red-light)'; badge.style.color = 'var(--red)'; } } var h = document.getElementById('cms-smtp-host'); if (h) h.value = data.host || ''; var p = document.getElementById('cms-smtp-port'); if (p) p.value = data.port || '587'; var u = document.getElementById('cms-smtp-user'); if (u) u.value = data.user || ''; var f = document.getElementById('cms-smtp-from'); if (f) f.value = data.from || ''; }) .catch(function() {}); } function saveSmtp() { var host = document.getElementById('cms-smtp-host').value.trim(); var port = document.getElementById('cms-smtp-port').value.trim(); var user = document.getElementById('cms-smtp-user').value.trim(); var pass = document.getElementById('cms-smtp-pass').value.trim(); var from = document.getElementById('cms-smtp-from').value.trim(); var secure = document.getElementById('cms-smtp-secure').value; if (!host) { showToast('SMTP host required', 'error'); return; } fetch('/api/admin/config/smtp', { method: 'PUT', headers: getAuthHeaders(), body: JSON.stringify({ host: host, port: port, user: user, pass: pass, from: from, secure: secure === 'true' }) }) .then(function(r) { return r.json(); }) .then(function(data) { if (data.success) { showToast('SMTP settings saved', 'success'); loadSmtp(); } else showToast(data.error || 'Save failed', 'error'); }) .catch(function() { showToast('Request failed', 'error'); }); } function clearSmtp() { if (!confirm('Clear DB SMTP settings? Env vars will be used if set.')) return; fetch('/api/admin/config/smtp', { method: 'DELETE', headers: getAuthHeaders() }) .then(function(r) { return r.json(); }) .then(function(data) { if (data.success) { showToast('SMTP DB settings cleared', 'info'); loadSmtp(); } else showToast(data.error || 'Failed', 'error'); }) .catch(function() { showToast('Request failed', 'error'); }); } // ---- AUTO-DELETE / RESET ---- function saveAutoDelete() { var days = document.getElementById('cms-auto-delete-days').value; putConfig('site.auto_delete_days', days) .then(function() { var el = document.getElementById('admin-auto-delete-days'); if (el) el.textContent = days; showToast('Auto-delete set to ' + days + ' days', 'success'); }) .catch(function() { showToast('Save failed', 'error'); }); } function resetAllDefaults() { if (!confirm('Reset all settings to factory defaults?\n\nAnnouncements, feature flags, email templates will be reset. SMTP and custom models are preserved.')) return; fetch('/api/admin/config/reset-defaults', { method: 'POST', headers: getAuthHeaders() }) .then(function(r) { return r.json(); }) .then(function(data) { if (data.success) { showToast(data.message || 'Reset to defaults', 'success'); cmsLoaded = false; loadCms(); } else showToast(data.error || 'Reset failed', 'error'); }) .catch(function() { showToast('Request failed', 'error'); }); } // ---- HELPERS ---- function putConfig(key, value) { return fetch('/api/admin/config/' + encodeURIComponent(key), { method: 'PUT', headers: getAuthHeaders(), body: JSON.stringify({ value: value }) }) .then(function(r) { return r.json(); }) .then(function(data) { if (!data.success) throw new Error(data.error || 'Failed'); return data; }); } })(); // ============================================================ // ADMIN MODEL MANAGEMENT — Discover, search, enable/disable, custom models // ============================================================ (function() { document.addEventListener('tabChanged', function(e) { if (e.detail && e.detail.tab === 'admin') { loadAdminModels(); } }); document.addEventListener('click', function(e) { if (e.target.closest('#btn-discover-models')) discoverModels(); if (e.target.closest('#btn-add-custom-model')) addCustomModel(); if (e.target.closest('#btn-save-default-model')) saveDefaultModel(); if (e.target.closest('#btn-clear-all-models')) clearAllModels(); }); // Allow Enter key to trigger search document.addEventListener('keydown', function(e) { if (e.target.id === 'admin-model-search' && e.key === 'Enter') { e.preventDefault(); discoverModels(); } }); function esc(str) { if (!str) return ''; return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function loadAdminModels() { fetch('/api/admin/config/models', { headers: getAuthHeaders() }) .then(function(r) { return r.json(); }) .then(function(data) { if (!data.success) return; // Provider badge var badge = document.getElementById('admin-model-provider-badge'); if (badge) badge.textContent = (data.provider || 'unknown').toUpperCase(); // Default model selector — built-ins + custom models var defaultSel = document.getElementById('admin-default-model'); if (defaultSel) { defaultSel.innerHTML = ''; data.models.forEach(function(m) { var opt = document.createElement('option'); opt.value = m.id; opt.textContent = m.name + (m.enabled === false ? ' (disabled)' : ''); defaultSel.appendChild(opt); }); (data.custom || []).forEach(function(m) { var opt = document.createElement('option'); opt.value = m.id; opt.textContent = m.name; defaultSel.appendChild(opt); }); // Pre-select the saved default if (data.defaultModel) defaultSel.value = data.defaultModel; } // Built-in models with toggle var container = document.getElementById('admin-builtin-models'); if (container) { if (data.litellmHint) { container.innerHTML = '
' + '

LiteLLM mode: No built-in models. ' + 'Use Search API below to discover models from your proxy, then add them.

' + '
'; } else if (data.models.length === 0) { container.innerHTML = '

No built-in models for this provider.

'; } else { container.innerHTML = data.models.map(function(m) { var checked = m.enabled !== false ? 'checked' : ''; return ''; }).join(''); container.querySelectorAll('.admin-model-toggle').forEach(function(cb) { cb.addEventListener('change', function() { toggleModel(cb.dataset.modelId, cb.checked); }); }); } } // Custom models list renderCustomModels(data.custom || []); }) .catch(function(err) { console.error('[AdminModels] Load failed:', err); }); } function toggleModel(modelId, enabled) { fetch('/api/admin/config/models/toggle', { method: 'PUT', headers: getAuthHeaders(), body: JSON.stringify({ modelId: modelId, enabled: enabled }) }) .then(function(r) { return r.json(); }) .then(function(data) { if (data.success) { showToast(modelId + ' ' + (enabled ? 'enabled' : 'disabled'), 'success'); } else { showToast(data.error || 'Failed', 'error'); // Revert checkbox var cb = document.querySelector('.admin-model-toggle[data-model-id="' + modelId + '"]'); if (cb) cb.checked = !enabled; } }) .catch(function() { showToast('Request failed', 'error'); }); } function discoverModels() { var search = (document.getElementById('admin-model-search') || {}).value || ''; var container = document.getElementById('admin-discovered-models'); var hint = document.getElementById('admin-discover-hint'); if (!container) return; container.innerHTML = '

Querying provider API...

'; if (hint) hint.style.display = 'none'; fetch('/api/admin/config/models/discover?q=' + encodeURIComponent(search), { headers: getAuthHeaders() }) .then(function(r) { return r.json(); }) .then(function(data) { if (!data.success) { container.innerHTML = '

Error: ' + esc(data.error || 'Unknown error') + '

'; return; } if (!data.models || data.models.length === 0) { container.innerHTML = '

No models found' + (search ? ' matching "' + esc(search) + '"' : '') + '. Try a different search term.

'; return; } container.innerHTML = '

Found ' + data.count + ' models. Click + to add to your model list.

' + data.models.slice(0, 100).map(function(m) { return '
' + '' + '' + esc(m.name) + ' (' + esc(m.id) + ')' + '' + esc(m.cost || '') + '' + '' + esc(m.category || '') + '' + '
'; }).join(''); container.querySelectorAll('.admin-add-discovered').forEach(function(btn) { btn.addEventListener('click', function() { addDiscoveredModel(btn.dataset.mid, btn.dataset.mname, btn.dataset.mcost, btn.dataset.mcat, btn); }); }); }) .catch(function(err) { container.innerHTML = '

Request failed: ' + esc(err.message) + '

'; }); } function addDiscoveredModel(id, name, cost, category, btn) { fetch('/api/admin/config/models/add-discovered', { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ id: id, name: name, cost: cost, category: category }) }) .then(function(r) { return r.json(); }) .then(function(data) { if (data.success) { showToast('Added: ' + name + ' — now select it as default and click Set Default', 'success'); if (btn) { btn.textContent = 'Added'; btn.disabled = true; btn.style.background = 'var(--green)'; } // Refresh model lists, then auto-select the newly added model fetch('/api/admin/config/models', { headers: getAuthHeaders() }) .then(function(r) { return r.json(); }) .then(function(refreshed) { if (!refreshed.success) return; var defaultSel = document.getElementById('admin-default-model'); if (defaultSel) { defaultSel.innerHTML = ''; refreshed.models.forEach(function(m) { var opt = document.createElement('option'); opt.value = m.id; opt.textContent = m.name + (m.enabled === false ? ' (disabled)' : ''); defaultSel.appendChild(opt); }); (refreshed.custom || []).forEach(function(m) { var opt = document.createElement('option'); opt.value = m.id; opt.textContent = m.name; defaultSel.appendChild(opt); }); // Auto-select the model just added defaultSel.value = id; } renderCustomModels(refreshed.custom || []); }); } else { showToast(data.error || 'Failed', 'error'); } }) .catch(function() { showToast('Request failed', 'error'); }); } function addCustomModel() { var id = (document.getElementById('admin-custom-model-id') || {}).value || ''; var name = (document.getElementById('admin-custom-model-name') || {}).value || ''; var cost = (document.getElementById('admin-custom-model-cost') || {}).value || ''; var cat = (document.getElementById('admin-custom-model-cat') || {}).value || 'smart'; if (!id.trim() || !name.trim()) { showToast('Model ID and name required', 'error'); return; } fetch('/api/admin/config/models/custom', { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ id: id.trim(), name: name.trim(), cost: cost.trim(), category: cat }) }) .then(function(r) { return r.json(); }) .then(function(data) { if (data.success) { showToast('Custom model added: ' + name, 'success'); // Clear inputs var el = document.getElementById('admin-custom-model-id'); if (el) el.value = ''; el = document.getElementById('admin-custom-model-name'); if (el) el.value = ''; el = document.getElementById('admin-custom-model-cost'); if (el) el.value = ''; loadAdminModels(); } else { showToast(data.error || 'Failed', 'error'); } }) .catch(function() { showToast('Request failed', 'error'); }); } function renderCustomModels(custom) { var container = document.getElementById('admin-custom-models-list'); if (!container) return; if (!custom || custom.length === 0) { container.innerHTML = ''; return; } container.innerHTML = '' + custom.map(function(m) { return '
' + '' + esc(m.name) + ' (' + esc(m.id) + ')' + '' + esc(m.tag || 'CUSTOM') + '' + '' + esc(m.cost || '') + '' + '' + '
'; }).join(''); container.querySelectorAll('.admin-delete-custom').forEach(function(btn) { btn.addEventListener('click', function() { deleteCustomModel(btn.dataset.mid); }); }); } function deleteCustomModel(modelId) { fetch('/api/admin/config/models/custom/' + encodeURIComponent(modelId), { method: 'DELETE', headers: getAuthHeaders() }) .then(function(r) { return r.json(); }) .then(function(data) { if (data.success) { showToast('Removed: ' + modelId, 'info'); loadAdminModels(); } else { showToast(data.error || 'Failed', 'error'); } }) .catch(function() { showToast('Request failed', 'error'); }); } function clearAllModels() { if (!confirm('Remove all added models? You will need to re-add them via Search API.')) return; Promise.all([ fetch('/api/admin/config/models/clear-all', { method: 'POST', headers: getAuthHeaders() }) .then(function(r) { return r.json(); }) ]).then(function(results) { if (results[0].success) { showToast('All models cleared', 'info'); loadAdminModels(); } else { showToast(results[0].error || 'Failed', 'error'); } }).catch(function() { showToast('Request failed', 'error'); }); } function saveDefaultModel() { var sel = document.getElementById('admin-default-model'); if (!sel || !sel.value) { showToast('Select a model', 'error'); return; } fetch('/api/admin/config/models/default', { method: 'PUT', headers: getAuthHeaders(), body: JSON.stringify({ modelId: sel.value }) }) .then(function(r) { return r.json(); }) .then(function(data) { if (data.success) showToast('Default model set: ' + sel.value, 'success'); else showToast(data.error || 'Failed', 'error'); }) .catch(function() { showToast('Request failed', 'error'); }); } })();