pediatric-ai-scribe-v3/public/js/admin.js
Daniel f39f906fa5 fix(admin): restore SSO settings loading on admin panel
loadOidcConfig() was called from the first IIFE in admin.js but declared
in the second IIFE. Function declarations don't cross IIFE boundaries,
so the call threw ReferenceError and left the SSO form empty with
"Disabled" selected — even when SSO was configured and working. Moved
the call into the tabChanged handler of the IIFE where the function
lives.
2026-04-21 22:19:50 +02:00

1533 lines
69 KiB
JavaScript

// ============================================================
// 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();
// loadOidcConfig() is registered in the CMS IIFE below — it's not in scope here
}
// ---- 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 <strong style="color:' + (enabled ? 'var(--green)' : 'var(--red)') + '">' + (enabled ? '✅ Enabled' : '❌ Disabled') + '</strong>';
}
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 = '<tr><td colspan="5" style="text-align:center;color:var(--g400);padding:20px;">Loading...</td></tr>';
fetch('/api/admin/users', { headers: getAuthHeaders() })
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.success) { tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;color:var(--red);padding:20px;">Failed to load users</td></tr>'; return; }
renderUsers(data.users || []);
})
.catch(function() { tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;color:var(--red);padding:20px;">Request failed</td></tr>'; });
}
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 = '<tr><td colspan="5" style="text-align:center;color:var(--g400);padding:20px;">No users found</td></tr>';
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('<button class="btn-sm btn-primary admin-action" data-action="verify" data-id="' + u.id + '" data-email="' + esc(u.email) + '">Verify</button>');
}
if (u.disabled) {
actions.push('<button class="btn-sm btn-success admin-action" data-action="enable" data-id="' + u.id + '" data-email="' + esc(u.email) + '">Enable</button>');
} else {
actions.push('<button class="btn-sm btn-ghost admin-action" data-action="disable" data-id="' + u.id + '" data-email="' + esc(u.email) + '">Disable</button>');
}
if (u.role === 'admin') {
actions.push('<button class="btn-sm btn-ghost admin-action" data-action="demote" data-id="' + u.id + '" data-email="' + esc(u.email) + '">Demote</button>');
} else {
actions.push('<button class="btn-sm btn-primary admin-action" data-action="promote" data-id="' + u.id + '" data-email="' + esc(u.email) + '">Make Admin</button>');
}
if (u.role !== 'moderator') {
actions.push('<button class="btn-sm btn-ghost admin-action" data-action="set-moderator" data-id="' + u.id + '" data-email="' + esc(u.email) + '" style="color:var(--purple);">Set Moderator</button>');
}
if (u.role === 'moderator') {
actions.push('<button class="btn-sm btn-ghost admin-action" data-action="set-user" data-id="' + u.id + '" data-email="' + esc(u.email) + '">Set User</button>');
}
actions.push('<button class="btn-sm btn-ghost admin-action" data-action="reset-pw" data-id="' + u.id + '" data-email="' + esc(u.email) + '">Reset PW</button>');
actions.push('<button class="btn-sm admin-action" style="background:var(--red-light);color:var(--red);" data-action="delete" data-id="' + u.id + '" data-email="' + esc(u.email) + '" data-name="' + esc(u.name) + '">Delete</button>');
} else {
actions.push('<span style="font-size:12px;color:var(--g400);">(you)</span>');
}
return '<tr>' +
'<td><div style="font-weight:600;font-size:13px;">' + esc(u.name) + '</div><div style="font-size:11px;color:var(--g500);">' + esc(u.email) + '</div></td>' +
'<td><span style="font-size:12px;font-weight:600;color:' + roleColor + ';">' + (u.role || 'user') + '</span></td>' +
'<td><span style="font-size:12px;color:' + statusColor + ';">' + statusText + '</span></td>' +
'<td style="font-size:12px;color:var(--g500);">' + joined + '</td>' +
'<td style="white-space:nowrap;">' + actions.join(' ') + '</td>' +
'</tr>';
}).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':
showConfirm('Verify email for ' + email + '?', function() {
adminPost('/api/admin/users/' + userId + '/verify', {}, function() {
showToast(email + ' verified', 'success'); loadUsers();
});
});
break;
case 'disable':
showConfirm('Disable account for ' + email + '?', function() {
adminPost('/api/admin/users/' + userId + '/disable', {}, function() {
showToast(email + ' disabled', 'info'); loadUsers();
});
}, { danger: true, confirmText: 'Disable' });
break;
case 'enable':
adminPost('/api/admin/users/' + userId + '/enable', {}, function() {
showToast(email + ' enabled', 'success'); loadUsers();
});
break;
case 'promote':
showConfirm('Grant admin role to ' + email + '?', function() {
adminPost('/api/admin/users/' + userId + '/role', { role: 'admin' }, function() {
showToast(email + ' is now admin', 'success'); loadUsers();
});
});
break;
case 'demote':
showConfirm('Remove admin role from ' + email + '?', function() {
adminPost('/api/admin/users/' + userId + '/role', { role: 'user' }, function() {
showToast(email + ' demoted to user', 'info'); loadUsers();
});
});
break;
case 'set-moderator':
showConfirm('Set ' + email + ' as moderator? They will be able to manage Learning Hub content.', function() {
adminPost('/api/admin/users/' + userId + '/role', { role: 'moderator' }, function() {
showToast(email + ' is now a moderator', 'success'); loadUsers();
});
});
break;
case 'set-user':
showConfirm('Set ' + email + ' to regular user role?', function() {
adminPost('/api/admin/users/' + userId + '/role', { role: 'user' }, function() {
showToast(email + ' is now a user', 'success'); loadUsers();
});
});
break;
case 'reset-pw':
showConfirm('New password for ' + email + ' (8+ characters):', function(val) {
if (val.length < 8) { showToast('Password must be 8+ characters', 'error'); return; }
adminPost('/api/admin/users/' + userId + '/reset-password', { newPassword: val }, function() {
showToast('Password reset for ' + email, 'success');
});
}, { input: true, inputType: 'password', placeholder: 'New password', required: true, requiredMsg: 'Enter a password', confirmText: 'Reset Password' });
break;
case 'delete':
showConfirm('Permanently delete user ' + (name || email) + '? This cannot be undone.', function() {
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'); });
}, { danger: true, confirmText: 'Delete' });
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
})();
// ============================================================
// 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(); loadOidcConfig(); 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 sendBtn = document.getElementById('btn-test-email');
// Show inline email input instead of prompt()
var existing = document.getElementById('admin-test-email-row');
if (existing) { existing.remove(); return; }
var row = document.createElement('div');
row.id = 'admin-test-email-row';
row.style.cssText = 'margin-top:8px;display:flex;gap:6px;align-items:center;';
row.innerHTML = '<input type="email" id="admin-test-email-input" placeholder="recipient@example.com" style="padding:5px 8px;border:1px solid var(--g300);border-radius:6px;font-size:12px;width:220px;">'
+ '<button class="btn-sm btn-primary" id="admin-test-email-submit" style="font-size:11px;">Send</button>'
+ '<button class="btn-sm btn-ghost" id="admin-test-email-cancel" style="font-size:11px;">Cancel</button>';
if (sendBtn) sendBtn.parentElement.appendChild(row);
document.getElementById('admin-test-email-input').focus();
document.getElementById('admin-test-email-submit').onclick = function() {
var to = document.getElementById('admin-test-email-input').value.trim();
if (!to) { showToast('Enter an email address', 'error'); return; }
fetch('/api/admin/config/test-email', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ to: to, template: template })
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) { showToast('Test email sent to ' + to, 'success'); row.remove(); }
else showToast(data.error || 'Send failed', 'error');
})
.catch(function() { showToast('Request failed', 'error'); });
};
document.getElementById('admin-test-email-cancel').onclick = function() { row.remove(); };
}
// ---- AI PROMPTS ----
// ---- OIDC / SSO CONFIG ----
function loadOidcConfig() {
// Set callback URL display
var callbackEl = document.getElementById('oidc-callback-url');
if (callbackEl) callbackEl.textContent = window.location.origin + '/api/auth/oidc/callback';
fetch('/api/auth/oidc/config', { headers: getAuthHeaders() })
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.success) return;
var c = data.config || {};
var el;
el = document.getElementById('oidc-enabled'); if (el) el.value = c['oidc.enabled'] || 'false';
el = document.getElementById('oidc-issuer'); if (el) el.value = c['oidc.issuer'] || '';
el = document.getElementById('oidc-client-id'); if (el) el.value = c['oidc.client_id'] || '';
el = document.getElementById('oidc-client-secret'); if (el) el.placeholder = c['oidc.client_secret'] ? c['oidc.client_secret'] : 'Enter client secret';
el = document.getElementById('oidc-button-label'); if (el) el.value = c['oidc.button_label'] || '';
el = document.getElementById('oidc-disable-local'); if (el) el.value = c['oidc.disable_local_auth'] || 'false';
})
.catch(function() {});
}
document.addEventListener('click', function(e) {
if (e.target.id === 'btn-save-oidc' || e.target.closest('#btn-save-oidc')) {
e.preventDefault();
var status = document.getElementById('oidc-save-status');
if (status) { status.textContent = 'Saving...'; status.style.color = 'var(--g500)'; }
var body = {
'oidc.enabled': document.getElementById('oidc-enabled').value,
'oidc.issuer': document.getElementById('oidc-issuer').value.trim(),
'oidc.client_id': document.getElementById('oidc-client-id').value.trim(),
'oidc.button_label': document.getElementById('oidc-button-label').value.trim(),
'oidc.disable_local_auth': document.getElementById('oidc-disable-local').value
};
// Only send secret if user typed a new one
var secretEl = document.getElementById('oidc-client-secret');
if (secretEl && secretEl.value) body['oidc.client_secret'] = secretEl.value;
fetch('/api/auth/oidc/config', {
method: 'PUT',
headers: getAuthHeaders(),
body: JSON.stringify(body)
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) {
showToast('OIDC settings saved', 'success');
if (status) status.textContent = '';
if (secretEl) secretEl.value = '';
loadOidcConfig();
} else {
showToast(data.error || 'Save failed', 'error');
if (status) status.textContent = '';
}
})
.catch(function() { showToast('Save failed', 'error'); if (status) status.textContent = ''; });
}
});
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;
showConfirm('Reset "' + key + '" to the hardcoded default? This cannot be undone.', function() {
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() {
showConfirm('Clear DB SMTP settings? Env vars will be used if set.', function() {
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() {
showConfirm('Reset all settings to factory defaults? Announcements, feature flags, email templates will be reset. SMTP and custom models are preserved.', function() {
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'); });
}, { danger: true, confirmText: 'Reset' });
}
// ---- 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();
if (e.target.closest('.admin-model-test-btn')) {
var btn = e.target.closest('.admin-model-test-btn');
testModel(btn.dataset.mid, btn);
}
});
// 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
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 = '<div style="padding:10px 12px;background:var(--g50);border-radius:6px;font-size:13px;color:var(--g600);">' +
'<p style="margin:0 0 8px;"><i class="fas fa-info-circle" style="color:var(--blue);"></i> <strong>LiteLLM mode:</strong> No built-in models. ' +
'Use <strong>Search API</strong> below to discover models from your proxy, then add them.</p>' +
'<button id="btn-clear-all-models" class="btn-sm" style="background:var(--red-light);color:var(--red);border:none;border-radius:6px;padding:4px 12px;font-size:12px;cursor:pointer;">' +
'<i class="fas fa-trash"></i> Clear all added models</button></div>';
} else if (data.models.length === 0) {
container.innerHTML = '<p style="color:var(--g400);font-size:13px;">No built-in models for this provider.</p>';
} else {
container.innerHTML = data.models.map(function(m) {
var checked = m.enabled !== false ? 'checked' : '';
return '<div style="display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;background:var(--g50);font-size:13px;">' +
'<label style="display:flex;align-items:center;gap:8px;flex:1;cursor:pointer;margin:0;">' +
'<input type="checkbox" class="admin-model-toggle" data-model-id="' + esc(m.id) + '" ' + checked + ' style="accent-color:var(--blue);">' +
'<span style="flex:1;"><strong>' + esc(m.name) + '</strong> <span style="color:var(--g500);font-size:11px;">(' + esc(m.id) + ')</span></span>' +
'<span style="font-size:11px;padding:1px 6px;border-radius:4px;background:var(--g100);color:var(--g600);">' + esc(m.tag || '') + '</span>' +
'<span style="font-size:11px;color:var(--g500);">' + esc(m.cost || '') + '</span>' +
'</label>' +
'<button class="btn-sm admin-model-test-btn" data-mid="' + esc(m.id) + '" style="padding:2px 8px;font-size:11px;background:var(--g100);color:var(--g700);border:none;border-radius:4px;cursor:pointer;white-space:nowrap;">Test</button>' +
'</div>';
}).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 = '<p style="color:var(--g400);font-size:13px;"><i class="fas fa-spinner fa-spin"></i> Querying provider API...</p>';
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 = '<p style="color:var(--red);font-size:13px;">Error: ' + esc(data.error || 'Unknown error') + '</p>';
return;
}
if (!data.models || data.models.length === 0) {
container.innerHTML = '<p style="color:var(--g400);font-size:13px;">No models found' + (search ? ' matching "' + esc(search) + '"' : '') + '. Try a different search term.</p>';
return;
}
container.innerHTML = '<p style="font-size:12px;color:var(--g500);margin:0 0 6px;">Found ' + data.count + ' models. Click + to add to your model list.</p>' +
data.models.slice(0, 100).map(function(m) {
return '<div style="display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:6px;background:var(--g50);font-size:13px;">' +
'<button class="btn-sm btn-primary admin-add-discovered" data-mid="' + esc(m.id) + '" data-mname="' + esc(m.name) + '" data-mcost="' + esc(m.cost) + '" data-mcat="' + esc(m.category) + '" style="padding:2px 8px;font-size:11px;min-width:28px;">+</button>' +
'<button class="btn-sm admin-model-test-btn" data-mid="' + esc(m.id) + '" style="padding:2px 8px;font-size:11px;background:var(--g100);color:var(--g700);border:none;border-radius:4px;cursor:pointer;">Test</button>' +
'<span style="flex:1;"><strong>' + esc(m.name) + '</strong> <span style="color:var(--g500);font-size:11px;">(' + esc(m.id) + ')</span></span>' +
'<span style="font-size:11px;color:var(--g500);">' + esc(m.cost || '') + '</span>' +
'<span style="font-size:10px;padding:1px 5px;border-radius:3px;background:var(--g100);color:var(--g600);">' + esc(m.category || '') + '</span>' +
'</div>';
}).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 = '<p style="color:var(--red);font-size:13px;">Request failed: ' + esc(err.message) + '</p>';
});
}
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 = '<label style="font-size:12px;font-weight:600;color:var(--g600);display:block;margin-bottom:4px;">Custom / Discovered Models</label>' +
custom.map(function(m) {
return '<div style="display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:6px;background:var(--g50);font-size:13px;">' +
'<span style="flex:1;"><strong>' + esc(m.name) + '</strong> <span style="color:var(--g500);font-size:11px;">(' + esc(m.id) + ')</span></span>' +
'<span style="font-size:11px;padding:1px 6px;border-radius:4px;background:var(--blue-light, #e0f0ff);color:var(--blue);">' + esc(m.tag || 'CUSTOM') + '</span>' +
'<span style="font-size:11px;color:var(--g500);">' + esc(m.cost || '') + '</span>' +
'<button class="btn-sm admin-model-test-btn" data-mid="' + esc(m.id) + '" style="padding:2px 8px;font-size:11px;background:var(--g100);color:var(--g700);border:none;border-radius:4px;cursor:pointer;">Test</button>' +
'<button class="btn-sm admin-delete-custom" data-mid="' + esc(m.id) + '" style="padding:2px 8px;font-size:11px;background:var(--red-light);color:var(--red);border:none;border-radius:4px;cursor:pointer;">Remove</button>' +
'</div>';
}).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() {
showConfirm('Remove all added models? You will need to re-add them via Search API.', function() {
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'); });
}
function testModel(modelId, btn) {
if (!modelId) return;
var origText = btn ? btn.textContent : 'Test';
if (btn) { btn.textContent = '...'; btn.disabled = true; }
fetch('/api/admin/config/models/test', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ modelId: modelId })
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (btn) { btn.textContent = origText; btn.disabled = false; }
if (data.success) {
showToast('"' + (data.response || '?') + '" — ' + modelId + ' (' + (data.duration || 0) + 'ms)', 'success');
} else {
showToast('Test failed: ' + (data.error || 'Unknown error'), 'error');
}
})
.catch(function() {
if (btn) { btn.textContent = origText; btn.disabled = false; }
showToast('Request failed', 'error');
});
}
})();
// ============================================================
// ADMIN TTS MANAGEMENT
// ============================================================
(function() {
document.addEventListener('tabChanged', function(e) {
if (e.detail && e.detail.tab === 'admin') loadTTSConfig();
});
document.addEventListener('click', function(e) {
if (e.target.closest('#btn-test-tts')) testTTS();
if (e.target.closest('#btn-discover-tts')) discoverTTS();
if (e.target.closest('.admin-tts-set-btn')) {
var btn = e.target.closest('.admin-tts-set-btn');
setTTSDefault(btn.dataset.id, btn.dataset.type, btn);
}
});
document.addEventListener('keydown', function(e) {
if (e.target.id === 'admin-tts-search' && e.key === 'Enter') { e.preventDefault(); discoverTTS(); }
});
function esc(str) {
if (!str) return '';
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function loadTTSConfig() {
fetch('/api/admin/config/tts', { headers: getAuthHeaders() })
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.success) return;
var badge = document.getElementById('admin-tts-provider-badge');
if (badge) {
badge.textContent = (data.provider || 'none').toUpperCase();
badge.style.background = data.provider === 'none' ? 'var(--red-light)' : 'var(--g100)';
badge.style.color = data.provider === 'none' ? 'var(--red)' : 'var(--g600)';
}
var info = document.getElementById('admin-tts-info');
if (info) {
var parts = [];
if (data.envProvider !== 'auto') parts.push('TTS_PROVIDER=' + data.envProvider);
if (data.dbVoice) parts.push('DB voice: ' + data.dbVoice);
else if (data.envVoice) parts.push('Env voice: ' + data.envVoice);
if (data.dbModel) parts.push('DB model: ' + data.dbModel);
else if (data.envModel) parts.push('Env model: ' + data.envModel);
var configured = Object.keys(data.configured || {}).filter(function(k) { return data.configured[k]; });
if (configured.length) parts.push('Configured: ' + configured.join(', '));
info.textContent = parts.join(' · ') || 'Auto-detected from env';
}
var voiceSel = document.getElementById('admin-tts-voice');
if (voiceSel) {
voiceSel.innerHTML = '';
var voices = (data.voices && data.voices[data.provider]) || [];
if (data.currentVoice && voices.indexOf(data.currentVoice) === -1) voices = [data.currentVoice].concat(voices);
if (voices.length === 0) voices = ['default'];
voices.forEach(function(v) {
var opt = document.createElement('option');
opt.value = v;
opt.textContent = v + (v === data.currentVoice ? ' (active)' : '');
if (v === data.currentVoice) opt.selected = true;
voiceSel.appendChild(opt);
});
}
})
.catch(function() {});
}
function discoverTTS() {
var search = (document.getElementById('admin-tts-search') || {}).value || '';
var container = document.getElementById('admin-tts-discovered');
var hint = document.getElementById('admin-tts-discover-hint');
if (!container) return;
container.innerHTML = '<p style="font-size:13px;color:var(--g400);"><i class="fas fa-spinner fa-spin"></i> Querying provider...</p>';
if (hint) hint.style.display = 'none';
fetch('/api/admin/config/tts/discover?q=' + encodeURIComponent(search), { headers: getAuthHeaders() })
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.success) {
container.innerHTML = '<p style="font-size:13px;color:var(--red);">Error: ' + esc(data.error || 'Unknown') + '</p>';
return;
}
var items = data.voices || [];
if (items.length === 0) {
container.innerHTML = '<p style="font-size:13px;color:var(--g400);">No voices found' + (search ? ' matching "' + esc(search) + '"' : '') + '</p>';
return;
}
container.innerHTML = '<p style="font-size:12px;color:var(--g500);margin:0 0 6px;">Found ' + data.count + ' voices (provider: ' + esc(data.provider) + ')</p>' +
items.slice(0, 100).map(function(v) {
var isModel = (v.source || '').indexOf('gateway') !== -1 || (v.source || '').indexOf('builtin-model') !== -1;
var setType = isModel ? 'model' : 'voice';
var badge = isModel ? '<span style="font-size:9px;padding:1px 5px;border-radius:4px;background:var(--blue);color:white;margin-left:4px;">MODEL</span>' : '<span style="font-size:9px;padding:1px 5px;border-radius:4px;background:var(--green);color:white;margin-left:4px;">VOICE</span>';
return '<div style="display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:6px;background:var(--g50);font-size:13px;">' +
'<button class="btn-sm btn-primary admin-tts-set-btn" data-id="' + esc(v.id) + '" data-type="' + setType + '" style="padding:2px 8px;font-size:11px;">Set</button>' +
'<span style="flex:1;">' + esc(v.name) + badge + '</span>' +
'<span style="font-size:10px;color:var(--g400);">' + esc(v.source || '') + '</span>' +
'</div>';
}).join('');
})
.catch(function(err) {
container.innerHTML = '<p style="font-size:13px;color:var(--red);">Request failed: ' + esc(err.message) + '</p>';
});
}
function setTTSDefault(id, type, btn) {
var key = type === 'model' ? 'tts.model' : 'tts.voice';
var origText = btn ? btn.textContent : '';
if (btn) { btn.textContent = '...'; btn.disabled = true; }
fetch('/api/admin/config/' + encodeURIComponent(key), {
method: 'PUT',
headers: getAuthHeaders(),
body: JSON.stringify({ value: id })
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (btn) { btn.textContent = 'Set'; btn.disabled = false; btn.style.background = 'var(--green)'; setTimeout(function() { btn.style.background = ''; }, 2000); }
if (data.success) {
showToast('TTS ' + type + ' set to: ' + id, 'success');
// Update voice selector
var voiceSel = document.getElementById('admin-tts-voice');
if (voiceSel && type === 'voice') {
var found = Array.from(voiceSel.options).find(function(o) { return o.value === id; });
if (!found) {
var opt = document.createElement('option');
opt.value = id; opt.textContent = id + ' (active)';
voiceSel.insertBefore(opt, voiceSel.firstChild);
}
voiceSel.value = id;
}
loadTTSConfig();
} else {
showToast(data.error || 'Failed', 'error');
}
})
.catch(function() {
if (btn) { btn.textContent = origText; btn.disabled = false; }
showToast('Request failed', 'error');
});
}
function testTTS() {
var text = (document.getElementById('admin-tts-test-text') || {}).value || 'Hello.';
var voice = (document.getElementById('admin-tts-voice') || {}).value || '';
var btn = document.getElementById('btn-test-tts');
var resultEl = document.getElementById('admin-tts-result');
var audioEl = document.getElementById('admin-tts-audio');
if (btn) { btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Synthesizing...'; }
if (resultEl) resultEl.textContent = '';
if (audioEl) audioEl.style.display = 'none';
fetch('/api/admin/config/tts/test', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ text: text, voice: voice })
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fas fa-play"></i> Synthesize &amp; Play'; }
if (!data.success) {
if (resultEl) resultEl.textContent = 'Error: ' + (data.error || 'Unknown error');
return;
}
if (audioEl && data.audio) {
var blob = base64ToBlob(data.audio, 'audio/mpeg');
audioEl.src = URL.createObjectURL(blob);
audioEl.style.display = 'inline-block';
audioEl.play();
}
if (resultEl) resultEl.textContent = 'Provider: ' + (data.provider || '?') + ' · Voice: ' + (data.voice || '?');
})
.catch(function(err) {
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fas fa-play"></i> Synthesize &amp; Play'; }
if (resultEl) resultEl.textContent = 'Request failed: ' + err.message;
});
}
function base64ToBlob(base64, type) {
var binary = atob(base64);
var bytes = new Uint8Array(binary.length);
for (var i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
return new Blob([bytes], { type: type });
}
})();
// ============================================================
// ADMIN STT MANAGEMENT
// ============================================================
(function() {
var mediaRecorder = null;
var audioChunks = [];
var recording = false;
document.addEventListener('tabChanged', function(e) {
if (e.detail && e.detail.tab === 'admin') loadSTTConfig();
});
document.addEventListener('click', function(e) {
if (e.target.closest('#btn-stt-record')) toggleRecording();
if (e.target.closest('#btn-discover-stt')) discoverSTT();
if (e.target.closest('.admin-stt-set-btn')) {
var btn = e.target.closest('.admin-stt-set-btn');
setSTTDefault(btn.dataset.id, btn);
}
});
document.addEventListener('keydown', function(e) {
if (e.target.id === 'admin-stt-search' && e.key === 'Enter') { e.preventDefault(); discoverSTT(); }
});
function esc(str) {
if (!str) return '';
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function loadSTTConfig() {
fetch('/api/admin/config/stt', { headers: getAuthHeaders() })
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.success) return;
var badge = document.getElementById('admin-stt-provider-badge');
if (badge) {
badge.textContent = (data.provider || 'none').toUpperCase();
badge.style.background = data.provider === 'none' ? 'var(--red-light)' : 'var(--g100)';
badge.style.color = data.provider === 'none' ? 'var(--red)' : 'var(--g600)';
}
var info = document.getElementById('admin-stt-info');
if (info) {
var parts = [];
if (data.envProvider !== 'auto') parts.push('TRANSCRIBE_PROVIDER=' + data.envProvider);
if (data.dbModel) parts.push('DB model: ' + data.dbModel);
else if (data.envModel) parts.push('Env model: ' + data.envModel);
var configured = Object.keys(data.configured || {}).filter(function(k) { return data.configured[k]; });
if (configured.length) parts.push('Configured: ' + configured.join(', '));
info.textContent = parts.join(' · ') || 'Auto-detected from env';
}
})
.catch(function() {});
}
function discoverSTT() {
var search = (document.getElementById('admin-stt-search') || {}).value || '';
var container = document.getElementById('admin-stt-discovered');
var hint = document.getElementById('admin-stt-discover-hint');
if (!container) return;
container.innerHTML = '<p style="font-size:13px;color:var(--g400);"><i class="fas fa-spinner fa-spin"></i> Querying provider...</p>';
if (hint) hint.style.display = 'none';
fetch('/api/admin/config/stt/discover?q=' + encodeURIComponent(search), { headers: getAuthHeaders() })
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.success) {
container.innerHTML = '<p style="font-size:13px;color:var(--red);">Error: ' + esc(data.error || 'Unknown') + '</p>';
return;
}
var items = data.models || [];
if (items.length === 0) {
container.innerHTML = '<p style="font-size:13px;color:var(--g400);">No models found' + (search ? ' matching "' + esc(search) + '"' : '') + '</p>';
return;
}
container.innerHTML = '<p style="font-size:12px;color:var(--g500);margin:0 0 6px;">Found ' + data.count + ' models (provider: ' + esc(data.provider) + ')</p>' +
items.map(function(m) {
return '<div style="display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:6px;background:var(--g50);font-size:13px;">' +
'<button class="btn-sm btn-primary admin-stt-set-btn" data-id="' + esc(m.id) + '" style="padding:2px 8px;font-size:11px;">Set</button>' +
'<span style="flex:1;">' + esc(m.name || m.id) + '</span>' +
'<span style="font-size:10px;color:var(--g400);">' + esc(m.source || '') + '</span>' +
'</div>';
}).join('');
})
.catch(function(err) {
container.innerHTML = '<p style="font-size:13px;color:var(--red);">Request failed: ' + esc(err.message) + '</p>';
});
}
function setSTTDefault(modelId, btn) {
var origText = btn ? btn.textContent : '';
if (btn) { btn.textContent = '...'; btn.disabled = true; }
fetch('/api/admin/config/' + encodeURIComponent('stt.model'), {
method: 'PUT',
headers: getAuthHeaders(),
body: JSON.stringify({ value: modelId })
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (btn) { btn.textContent = 'Set'; btn.disabled = false; btn.style.background = data.success ? 'var(--green)' : ''; setTimeout(function() { if (btn) btn.style.background = ''; }, 2000); }
if (data.success) { showToast('STT model set to: ' + modelId, 'success'); loadSTTConfig(); }
else showToast(data.error || 'Failed', 'error');
})
.catch(function() {
if (btn) { btn.textContent = origText; btn.disabled = false; }
showToast('Request failed', 'error');
});
}
function toggleRecording() {
if (recording) {
stopRecording();
} else {
startRecording();
}
}
function startRecording() {
var btn = document.getElementById('btn-stt-record');
var status = document.getElementById('admin-stt-recording-status');
var resultEl = document.getElementById('admin-stt-result');
if (resultEl) resultEl.style.display = 'none';
navigator.mediaDevices.getUserMedia({ audio: true })
.then(function(stream) {
audioChunks = [];
mediaRecorder = new MediaRecorder(stream);
mediaRecorder.addEventListener('dataavailable', function(e) {
if (e.data.size > 0) audioChunks.push(e.data);
});
mediaRecorder.addEventListener('stop', function() {
stream.getTracks().forEach(function(t) { t.stop(); });
transcribeRecording();
});
mediaRecorder.start();
recording = true;
if (btn) { btn.innerHTML = '<i class="fas fa-stop"></i> Stop Recording'; btn.style.background = 'var(--red)'; btn.style.color = 'white'; }
if (status) status.textContent = 'Recording... (click Stop when done)';
})
.catch(function(err) {
if (status) status.textContent = 'Microphone access denied: ' + err.message;
});
}
function stopRecording() {
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
mediaRecorder.stop();
}
recording = false;
var btn = document.getElementById('btn-stt-record');
var status = document.getElementById('admin-stt-recording-status');
if (btn) { btn.innerHTML = '<i class="fas fa-microphone"></i> Start Recording'; btn.style.background = ''; btn.style.color = ''; }
if (status) status.textContent = 'Processing...';
}
function transcribeRecording() {
if (audioChunks.length === 0) return;
var blob = new Blob(audioChunks, { type: 'audio/webm' });
var reader = new FileReader();
reader.onloadend = function() {
var base64 = reader.result.split(',')[1];
fetch('/api/admin/config/stt/test', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ audioBase64: base64, mimeType: 'audio/webm' })
})
.then(function(r) { return r.json(); })
.then(function(data) {
var status = document.getElementById('admin-stt-recording-status');
var resultEl = document.getElementById('admin-stt-result');
var textEl = document.getElementById('admin-stt-text');
var metaEl = document.getElementById('admin-stt-meta');
if (status) status.textContent = '';
if (resultEl) resultEl.style.display = 'block';
if (data.success) {
if (textEl) textEl.textContent = data.text || '(empty result)';
if (metaEl) metaEl.textContent = 'Provider: ' + (data.provider || '?') + ' · ' + (data.duration || 0) + 'ms';
} else {
if (textEl) textEl.textContent = 'Error: ' + (data.error || 'Unknown error');
if (metaEl) metaEl.textContent = '';
}
})
.catch(function(err) {
var status = document.getElementById('admin-stt-recording-status');
if (status) status.textContent = 'Request failed: ' + err.message;
});
};
reader.readAsDataURL(blob);
}
})();
// ============================================================
// ADMIN EMBEDDING MODELS MANAGEMENT
// ============================================================
(function() {
document.addEventListener('tabChanged', function(e) {
if (e.detail && e.detail.tab === 'admin') loadEmbeddingConfig();
});
document.addEventListener('click', function(e) {
if (e.target.closest('#btn-test-embedding')) testEmbedding();
if (e.target.closest('#btn-discover-embeddings')) discoverEmbeddings();
if (e.target.closest('.admin-embed-set-btn')) {
var btn = e.target.closest('.admin-embed-set-btn');
setEmbeddingDefault(btn.dataset.id, btn.dataset.dims, btn);
}
});
document.addEventListener('keydown', function(e) {
if (e.target.id === 'admin-embed-search' && e.key === 'Enter') { e.preventDefault(); discoverEmbeddings(); }
});
function esc(str) {
if (!str) return '';
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function loadEmbeddingConfig() {
fetch('/api/admin/config/embeddings', { headers: getAuthHeaders() })
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.success) return;
var badge = document.getElementById('admin-embed-provider-badge');
if (badge) {
badge.textContent = (data.provider || 'none').toUpperCase();
badge.style.background = data.configured ? 'var(--g100)' : 'var(--red-light)';
badge.style.color = data.configured ? 'var(--g600)' : 'var(--red)';
}
var info = document.getElementById('admin-embed-info');
if (info) {
var parts = [];
if (data.dbModel) parts.push('DB model: ' + data.dbModel);
else if (data.envModel) parts.push('Env model: ' + data.envModel);
parts.push('Dims: ' + (data.currentDimensions || '?'));
if (!data.configured) parts.push('⚠️ Not configured');
info.textContent = parts.join(' · ');
}
var modelsEl = document.getElementById('admin-embed-models');
if (modelsEl && data.models) {
modelsEl.innerHTML = data.models.map(function(m) {
var isCurrent = m.id === data.currentModel;
return '<div style="display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:6px;background:var(--g50);font-size:13px;">' +
'<button class="btn-sm admin-embed-set-btn" data-id="' + esc(m.id) + '" data-dims="' + m.dims + '" style="padding:2px 8px;font-size:11px;background:var(--g100);color:var(--g700);border:none;border-radius:4px;cursor:pointer;">Set</button>' +
'<span style="flex:1;"><strong>' + esc(m.name) + '</strong> <span style="color:var(--g500);font-size:11px;">(' + m.dims + 'd)</span></span>' +
'<span style="font-size:11px;padding:1px 6px;border-radius:4px;background:var(--g100);color:var(--g600);">' + esc(m.tag || '') + '</span>' +
(isCurrent ? '<span style="font-size:11px;padding:1px 6px;border-radius:4px;background:var(--green-light,#d1fae5);color:var(--green);">ACTIVE</span>' : '') +
'</div>';
}).join('');
}
})
.catch(function() {});
}
function discoverEmbeddings() {
var search = (document.getElementById('admin-embed-search') || {}).value || '';
var container = document.getElementById('admin-embed-discovered');
var hint = document.getElementById('admin-embed-discover-hint');
if (!container) return;
container.innerHTML = '<p style="font-size:13px;color:var(--g400);"><i class="fas fa-spinner fa-spin"></i> Querying provider...</p>';
if (hint) hint.style.display = 'none';
fetch('/api/admin/config/embeddings/discover?q=' + encodeURIComponent(search), { headers: getAuthHeaders() })
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.success) {
container.innerHTML = '<p style="font-size:13px;color:var(--red);">Error: ' + esc(data.error || 'Unknown') + '</p>';
return;
}
var items = data.models || [];
if (items.length === 0) {
container.innerHTML = '<p style="font-size:13px;color:var(--g400);">No models found' + (search ? ' matching "' + esc(search) + '"' : '') + '</p>';
return;
}
container.innerHTML = '<p style="font-size:12px;color:var(--g500);margin:0 0 6px;">Found ' + data.count + ' models (provider: ' + esc(data.provider) + ')</p>' +
items.map(function(m) {
return '<div style="display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:6px;background:var(--g50);font-size:13px;">' +
'<button class="btn-sm btn-primary admin-embed-set-btn" data-id="' + esc(m.id) + '" data-dims="' + (m.dims || '') + '" style="padding:2px 8px;font-size:11px;">Set</button>' +
'<span style="flex:1;">' + esc(m.name || m.id) + (m.dims && m.dims !== '?' ? ' <span style="color:var(--g500);font-size:11px;">(' + m.dims + 'd)</span>' : '') + '</span>' +
'<span style="font-size:10px;color:var(--g400);">' + esc(m.source || '') + '</span>' +
'</div>';
}).join('');
})
.catch(function(err) {
container.innerHTML = '<p style="font-size:13px;color:var(--red);">Request failed: ' + esc(err.message) + '</p>';
});
}
function setEmbeddingDefault(modelId, dims, btn) {
var origText = btn ? btn.textContent : '';
if (btn) { btn.textContent = '...'; btn.disabled = true; }
var promises = [
fetch('/api/admin/config/' + encodeURIComponent('embeddings.model'), {
method: 'PUT', headers: getAuthHeaders(), body: JSON.stringify({ value: modelId })
}).then(function(r) { return r.json(); })
];
if (dims && dims !== '?') {
promises.push(
fetch('/api/admin/config/' + encodeURIComponent('embeddings.dimensions'), {
method: 'PUT', headers: getAuthHeaders(), body: JSON.stringify({ value: String(dims) })
}).then(function(r) { return r.json(); })
);
}
Promise.all(promises)
.then(function(results) {
var ok = results.every(function(r) { return r.success; });
if (btn) { btn.textContent = 'Set'; btn.disabled = false; btn.style.background = ok ? 'var(--green)' : ''; setTimeout(function() { if (btn) btn.style.background = ''; }, 2000); }
if (ok) { showToast('Embedding model set to: ' + modelId + (dims ? ' (' + dims + 'd)' : ''), 'success'); loadEmbeddingConfig(); }
else showToast(results[0].error || 'Failed', 'error');
})
.catch(function() {
if (btn) { btn.textContent = origText; btn.disabled = false; }
showToast('Request failed', 'error');
});
}
function testEmbedding() {
var text = (document.getElementById('admin-embed-test-text') || {}).value || 'test';
var resultEl = document.getElementById('admin-embed-result');
var btn = document.getElementById('btn-test-embedding');
if (btn) { btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>'; }
if (resultEl) resultEl.textContent = 'Generating...';
fetch('/api/admin/config/embeddings/test', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ text: text })
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fas fa-code-branch"></i> Generate'; }
if (!data.success) {
if (resultEl) resultEl.innerHTML = '<span style="color:var(--red);">Error: ' + esc(data.error || 'Failed') + '</span>';
return;
}
if (resultEl) {
resultEl.innerHTML =
'<strong>Dimensions:</strong> ' + data.dimensions + ' &nbsp;|&nbsp; ' +
'<strong>Model:</strong> ' + esc(data.model) + ' &nbsp;|&nbsp; ' +
'<strong>' + data.duration + 'ms</strong>' +
'<div style="margin-top:4px;font-family:monospace;font-size:11px;color:var(--g400);">Sample: [' + (data.sample || []).join(', ') + ', ...]</div>';
}
})
.catch(function(err) {
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fas fa-code-branch"></i> Generate'; }
if (resultEl) resultEl.textContent = 'Request failed: ' + err.message;
});
}
})();