- Full model management UI in admin panel: toggle models on/off, add custom model IDs (any OpenRouter/Bedrock ID), set admin-configured default model - /api/models now returns admin-set default model, frontend respects it - Toggle switch CSS for clean enable/disable UX - Backend already had the API endpoints, this adds the missing UI
751 lines
31 KiB
JavaScript
751 lines
31 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();
|
|
}
|
|
|
|
// ---- 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':
|
|
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, '>').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();
|
|
if (e.target.closest('#btn-add-custom-model')) addCustomModel();
|
|
if (e.target.closest('#btn-save-default-model')) saveDefaultModel();
|
|
});
|
|
|
|
// 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();
|
|
loadModels();
|
|
}
|
|
|
|
// ---- 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'); });
|
|
}
|
|
|
|
// ---- AI MODEL MANAGEMENT ----
|
|
|
|
var modelsLoaded = false;
|
|
|
|
function loadModels() {
|
|
if (modelsLoaded) return;
|
|
modelsLoaded = true;
|
|
|
|
fetch('/api/admin/config/models', { headers: getAuthHeaders() })
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
if (!data.success) return;
|
|
window._adminModelsData = data;
|
|
var providerEl = document.getElementById('admin-model-provider');
|
|
if (providerEl) providerEl.textContent = (data.provider || 'openrouter').toUpperCase();
|
|
renderModelTable(data.models || []);
|
|
renderCustomModels(data.custom || []);
|
|
populateDefaultModelSelect(data.models || [], data.custom || []);
|
|
})
|
|
.catch(function(err) { console.error('[Admin] Models load failed:', err); });
|
|
}
|
|
|
|
function renderModelTable(models) {
|
|
var tbody = document.getElementById('admin-models-body');
|
|
if (!tbody) return;
|
|
if (models.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;color:var(--g400);padding:20px;">No models</td></tr>';
|
|
return;
|
|
}
|
|
|
|
var catColors = { free: 'var(--green)', fast: 'var(--blue)', smart: 'var(--purple)', premium: 'var(--amber)' };
|
|
|
|
tbody.innerHTML = models.map(function(m) {
|
|
var catColor = catColors[m.category] || 'var(--g500)';
|
|
var checked = m.enabled ? 'checked' : '';
|
|
return '<tr>' +
|
|
'<td style="font-size:13px;"><div style="font-weight:600;">' + esc(m.name) + '</div><div style="font-size:11px;color:var(--g400);font-family:monospace;">' + esc(m.id) + '</div></td>' +
|
|
'<td><span style="font-size:12px;color:' + catColor + ';font-weight:600;text-transform:uppercase;">' + esc(m.category) + '</span></td>' +
|
|
'<td style="font-size:12px;color:var(--g600);">' + esc(m.cost || '?') + '</td>' +
|
|
'<td><span style="font-size:11px;padding:1px 6px;border-radius:4px;background:var(--g100);color:var(--g600);">' + esc(m.tag || '') + '</span></td>' +
|
|
'<td style="text-align:center;"><label class="toggle-switch"><input type="checkbox" class="model-toggle" data-model-id="' + esc(m.id) + '" ' + checked + '><span class="toggle-slider"></span></label></td>' +
|
|
'</tr>';
|
|
}).join('');
|
|
|
|
// Bind toggle events
|
|
tbody.querySelectorAll('.model-toggle').forEach(function(cb) {
|
|
cb.addEventListener('change', function() {
|
|
var modelId = cb.dataset.modelId;
|
|
var enabled = cb.checked;
|
|
toggleModel(modelId, enabled);
|
|
});
|
|
});
|
|
}
|
|
|
|
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((enabled ? 'Enabled' : 'Disabled') + ': ' + modelId, 'success');
|
|
} else {
|
|
showToast(data.error || 'Toggle failed', 'error');
|
|
}
|
|
})
|
|
.catch(function() { showToast('Request failed', 'error'); });
|
|
}
|
|
|
|
function renderCustomModels(custom) {
|
|
var container = document.getElementById('admin-custom-models-list');
|
|
if (!container) return;
|
|
if (custom.length === 0) {
|
|
container.innerHTML = '<div style="font-size:12px;color:var(--g400);padding:4px 0;">No custom models added</div>';
|
|
return;
|
|
}
|
|
container.innerHTML = custom.map(function(m) {
|
|
return '<div style="display:flex;align-items:center;gap:8px;padding:6px 10px;background:var(--g50);border-radius:6px;margin-bottom:4px;">' +
|
|
'<span style="font-size:13px;font-weight:600;flex:1;">' + esc(m.name) + '</span>' +
|
|
'<span style="font-size:11px;color:var(--g500);font-family:monospace;">' + esc(m.id) + '</span>' +
|
|
'<span style="font-size:11px;color:var(--g500);">' + esc(m.cost || '?') + '</span>' +
|
|
'<button class="btn-sm" style="background:var(--red-light);color:var(--red);border:none;border-radius:4px;padding:2px 8px;font-size:11px;cursor:pointer;" data-delete-model="' + esc(m.id) + '"><i class="fas fa-trash"></i></button>' +
|
|
'</div>';
|
|
}).join('');
|
|
|
|
container.querySelectorAll('[data-delete-model]').forEach(function(btn) {
|
|
btn.addEventListener('click', function() {
|
|
var id = btn.dataset.deleteModel;
|
|
if (!confirm('Remove custom model "' + id + '"?')) return;
|
|
deleteCustomModel(id);
|
|
});
|
|
});
|
|
}
|
|
|
|
function populateDefaultModelSelect(models, custom) {
|
|
var sel = document.getElementById('admin-default-model');
|
|
if (!sel) return;
|
|
sel.innerHTML = '<option value="">(Provider default)</option>';
|
|
var all = models.filter(function(m) { return m.enabled; }).concat(custom || []);
|
|
all.forEach(function(m) {
|
|
var opt = document.createElement('option');
|
|
opt.value = m.id;
|
|
opt.textContent = m.name + ' (' + (m.cost || '?') + ')';
|
|
sel.appendChild(opt);
|
|
});
|
|
|
|
// Load current default
|
|
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; });
|
|
if (cfg['models.default']) sel.value = cfg['models.default'];
|
|
});
|
|
}
|
|
|
|
function addCustomModel() {
|
|
var id = document.getElementById('admin-custom-id').value.trim();
|
|
var name = document.getElementById('admin-custom-name').value.trim();
|
|
var cost = document.getElementById('admin-custom-cost').value.trim();
|
|
if (!id || !name) { showToast('Model ID and name required', 'error'); return; }
|
|
|
|
fetch('/api/admin/config/models/custom', {
|
|
method: 'POST',
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify({ id: id, name: name, cost: cost || '?', category: 'smart' })
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
if (data.success) {
|
|
showToast('Custom model added: ' + name, 'success');
|
|
document.getElementById('admin-custom-id').value = '';
|
|
document.getElementById('admin-custom-name').value = '';
|
|
document.getElementById('admin-custom-cost').value = '';
|
|
modelsLoaded = false;
|
|
loadModels();
|
|
} else {
|
|
showToast(data.error || 'Add failed', 'error');
|
|
}
|
|
})
|
|
.catch(function() { showToast('Request failed', 'error'); });
|
|
}
|
|
|
|
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('Model removed', 'info');
|
|
modelsLoaded = false;
|
|
loadModels();
|
|
} else {
|
|
showToast(data.error || 'Delete failed', 'error');
|
|
}
|
|
})
|
|
.catch(function() { showToast('Request failed', 'error'); });
|
|
}
|
|
|
|
function saveDefaultModel() {
|
|
var sel = document.getElementById('admin-default-model');
|
|
if (!sel) return;
|
|
var val = sel.value;
|
|
putConfig('models.default', val)
|
|
.then(function() { showToast('Default model saved' + (val ? ': ' + val : ' (provider default)'), 'success'); })
|
|
.catch(function() { showToast('Save 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;
|
|
});
|
|
}
|
|
|
|
})();
|