This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/ui-admin.js
2026-02-25 21:38:49 +00:00

728 lines
38 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ==========================================
// Chat Switchboard UI Admin
// ==========================================
// Extends UI with admin modal tabs: users, stats, roles,
// usage, audit, providers, models, presets, teams, settings.
Object.assign(UI, {
// ── Admin Modal ──────────────────────────
openAdmin() { openModal('adminModal'); UI.switchAdminTab('users'); },
closeAdmin() { closeModal('adminModal'); },
openTeamAdmin() {
const adminTeams = (UI._myTeams || []).filter(t => t.my_role === 'admin');
if (adminTeams.length === 0) {
UI.toast('You are not an admin of any teams', 'error');
return;
}
openModal('teamAdminModal');
if (adminTeams.length === 1) {
// Skip picker, go directly to the team
UI.openTeamManage(adminTeams[0].id, adminTeams[0].name);
} else {
// Show team picker
document.getElementById('teamAdminTitle').textContent = 'Team Management';
document.getElementById('teamAdminPicker').style.display = '';
document.getElementById('teamAdminContent').style.display = 'none';
document.getElementById('teamAdminPickerList').innerHTML = adminTeams.map(t => `
<div class="team-card" style="cursor:pointer" onclick="UI.openTeamManage('${t.id}', '${esc(t.name)}')">
<div class="team-card-info">
<strong>${esc(t.name)}</strong>
<span class="badge-admin">admin</span>
<span style="margin-left:auto;color:var(--text-3);font-size:12px">Manage →</span>
</div>
${t.description ? `<div class="text-muted" style="font-size:12px">${esc(t.description)}</div>` : ''}
<div class="text-muted" style="font-size:11px">${t.member_count} member${t.member_count !== 1 ? 's' : ''}</div>
</div>
`).join('');
}
},
closeTeamAdmin() { closeModal('teamAdminModal'); },
switchTeamTab(tab) {
const tabs = document.querySelectorAll('#teamAdminTabs .admin-tab');
tabs.forEach(t => {
t.classList.remove('active');
if (t.dataset.ttab === tab) t.classList.add('active');
});
document.querySelectorAll('.team-tab-content').forEach(c => c.style.display = 'none');
const panel = document.getElementById(`teamTab${tab.charAt(0).toUpperCase() + tab.slice(1)}`);
if (panel) panel.style.display = '';
// Lazy-load tab data
const teamId = UI._managingTeamId;
if (tab === 'members') UI.loadTeamManageMembers(teamId);
if (tab === 'providers') UI.loadTeamManageProviders(teamId);
if (tab === 'presets') UI.loadTeamManagePresets(teamId);
if (tab === 'usage') UI.loadTeamUsage();
if (tab === 'activity') UI.loadTeamAuditLog(1);
},
async switchAdminTab(tab) {
document.querySelectorAll('#adminModal .admin-tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tab));
document.querySelectorAll('.admin-tab-content').forEach(c => c.style.display = 'none');
const panel = document.getElementById(`admin${tab.charAt(0).toUpperCase() + tab.slice(1)}Tab`);
if (panel) panel.style.display = '';
if (tab === 'users') await this.loadAdminUsers();
if (tab === 'stats') await this.loadAdminStats();
if (tab === 'audit') await this.loadAuditLog();
if (tab === 'providers') await this.loadAdminProviders();
if (tab === 'models') await this.loadAdminModels();
if (tab === 'presets') await this.loadAdminPresets();
if (tab === 'teams') await this.loadAdminTeams();
if (tab === 'settings') await this.loadAdminSettings();
if (tab === 'roles') await this.loadAdminRoles();
if (tab === 'usage') await this.loadAdminUsage();
if (tab === 'extensions') await this.loadAdminExtensions();
if (tab === 'storage' && typeof loadAdminStorage === 'function') await loadAdminStorage();
},
async loadAdminUsers(quiet) {
const el = document.getElementById('adminUserList');
if (!quiet) el.innerHTML = '<div class="loading">Loading...</div>';
try {
const resp = await API.adminListUsers();
const users = resp.users || resp.data || [];
el.innerHTML = users.map(u => {
const teamBadges = (u.teams || []).map(t =>
`<span class="badge-team" title="${esc(t.role)}">${esc(t.team_name)}</span>`
).join(' ');
const isPending = !u.is_active;
return `
<div class="admin-user-row${isPending ? ' user-inactive' : ''}">
<div class="admin-user-info">
<div><strong>${esc(u.username)}</strong> <span class="badge-${u.role}">${u.role}</span>
${isPending ? '<span class="badge-pending">pending</span>' : ''}
${teamBadges}</div>
<div class="admin-user-email">${esc(u.email)}</div>
</div>
<div class="admin-user-actions">
${isPending
? `<button class="btn-approve" onclick="showApproveForm('${u.id}', '${esc(u.username)}')">Approve</button>`
: `<button onclick="toggleUserActive('${u.id}', false)">Disable</button>`
}
<button onclick="toggleUserRole('${u.id}', '${u.role === 'admin' ? 'user' : 'admin'}')">${u.role === 'admin' ? 'Demote' : 'Promote'}</button>
<button onclick="adminResetUserPassword('${u.id}', '${esc(u.username)}')">Reset PW</button>
<button class="btn-danger" onclick="deleteUser('${u.id}', '${esc(u.username)}')">Delete</button>
</div>
</div>
<div class="admin-approve-form" id="approveForm-${u.id}" style="display:none">
<div class="approve-teams" id="approveTeams-${u.id}"></div>
<div class="form-row" style="margin-top:8px">
<button class="btn-small btn-primary" onclick="submitApproval('${u.id}')">Activate & Assign</button>
<button class="btn-small" onclick="hideApproveForm('${u.id}')">Cancel</button>
</div>
</div>`;
}).join('') || '<div class="empty-hint">No users</div>';
} catch (e) { el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`; }
},
async loadAdminStats() {
const el = document.getElementById('adminStats');
el.innerHTML = '<div class="loading">Loading...</div>';
try {
const s = await API.adminGetStats();
const labels = { users: 'Users', channels: 'Channels', messages: 'Messages' };
el.innerHTML = '<div class="stats-grid">' +
Object.entries(s).map(([k, v]) => `
<div class="stat-card">
<div class="stat-value">${typeof v === 'number' ? v.toLocaleString() : esc(String(v))}</div>
<div class="stat-label">${labels[k] || k.replace(/_/g, ' ')}</div>
</div>`).join('') +
'</div>';
} catch (e) { el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`; }
},
// ── Admin: Roles ────────────────────────
async loadAdminRoles() {
const el = document.getElementById('adminRolesContent');
if (!el) return;
if (!UI._adminRoleConfig) {
UI._adminRoleConfig = renderRoleConfig(el, {
scope: 'admin',
showFallback: true,
modelIdField: 'model_id',
modelNameField: 'display_name',
providerIdField: 'provider_config_id',
fetchProviders: async () => {
const configs = await API.adminListGlobalConfigs();
return configs.configs || configs || [];
},
fetchModels: async () => {
const models = await API.adminListModels();
return models.models || models.data || models || [];
},
fetchRoles: () => API.adminListRoles(),
onSave: async (roleId, config) => {
await API.adminUpdateRole(roleId, config);
},
onTest: (roleId) => API.adminTestRole(roleId),
});
}
await UI._adminRoleConfig.refresh();
},
// ── Admin: Usage ────────────────────────
async loadAdminUsage() {
// Usage stats via primitive
const totalsEl = document.getElementById('adminUsageTotals');
if (!totalsEl) return;
// Wrap totals+results in a container for the primitive (first time only)
let usageContainer = document.getElementById('_adminUsageContainer');
if (!usageContainer) {
usageContainer = document.createElement('div');
usageContainer.id = '_adminUsageContainer';
const resultsEl = document.getElementById('adminUsageResults');
totalsEl.parentElement.insertBefore(usageContainer, totalsEl);
usageContainer.appendChild(totalsEl);
if (resultsEl) usageContainer.appendChild(resultsEl);
}
if (!UI._adminUsageDash) {
UI._adminUsageDash = renderUsageDashboard(usageContainer, {
apiFetch: (p) => API.adminGetUsage(p),
periodElId: 'usagePeriod',
groupByElId: 'usageGroupBy',
compact: false,
showUserColumn: true,
});
}
await UI._adminUsageDash.refresh();
// Pricing table (admin-only, separate from usage primitive)
const pricingEl = document.getElementById('adminPricingTable');
if (pricingEl) {
try {
const pricing = await API.adminListPricing();
const entries = pricing || [];
if (entries.length === 0) {
pricingEl.innerHTML = '<div class="empty-hint">No pricing configured. Sync providers to populate from catalog.</div>';
} else {
pricingEl.innerHTML = `
<table class="admin-table" style="margin-top:8px">
<thead><tr>
<th>Model</th>
<th style="text-align:right">Input $/M</th>
<th style="text-align:right">Output $/M</th>
<th>Source</th>
</tr></thead>
<tbody>${entries.map(p => `<tr>
<td>${esc(p.model_id)}</td>
<td style="text-align:right">${p.input_per_m != null ? '$' + Number(p.input_per_m).toFixed(4) : '—'}</td>
<td style="text-align:right">${p.output_per_m != null ? '$' + Number(p.output_per_m).toFixed(4) : '—'}</td>
<td><span class="badge-${p.source === 'manual' ? 'admin' : 'user'}">${p.source}</span></td>
</tr>`).join('')}</tbody>
</table>`;
}
} catch (e) { pricingEl.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`; }
}
},
async loadAdminExtensions() {
const el = document.getElementById('adminExtensionsList');
el.innerHTML = '<div class="loading">Loading...</div>';
try {
const resp = await API._get('/api/v1/admin/extensions');
const exts = resp.data || [];
this._adminExtensions = exts; // store for edit access
if (exts.length === 0) {
el.innerHTML = '<div class="text-muted" style="padding:16px">No extensions installed</div>';
return;
}
el.innerHTML = exts.map(ext => {
const statusBadge = ext.is_enabled
? '<span class="badge-admin" style="font-size:11px">enabled</span>'
: '<span class="badge-user" style="font-size:11px;opacity:0.5">disabled</span>';
const systemBadge = ext.is_system
? ' <span class="badge-user" style="font-size:11px">system</span>'
: '';
return `
<div class="admin-user-row" style="padding:10px 0;align-items:flex-start">
<div class="admin-user-info" style="flex:1">
<div style="display:flex;align-items:center;gap:8px">
<strong>${esc(ext.name)}</strong>
<code style="font-size:11px;color:var(--text-3)">${esc(ext.ext_id)}</code>
${statusBadge}${systemBadge}
</div>
<div class="text-muted" style="font-size:12px;margin-top:2px">
${esc(ext.description || 'No description')}
${ext.author ? ` · by ${esc(ext.author)}` : ''}
· v${esc(ext.version)} · ${ext.tier}
</div>
</div>
<div style="display:flex;gap:6px;flex-shrink:0">
<button class="btn-small" onclick="editAdminExtension('${ext.id}')">
Edit
</button>
<button class="btn-small" onclick="toggleAdminExtension('${ext.id}', ${!ext.is_enabled})">
${ext.is_enabled ? 'Disable' : 'Enable'}
</button>
<button class="btn-small btn-danger" onclick="deleteAdminExtension('${ext.id}', '${esc(ext.name)}')">
Uninstall
</button>
</div>
</div>`;
}).join('');
} catch (e) {
el.innerHTML = '<div class="text-muted" style="padding:16px">Failed to load extensions</div>';
}
},
_auditPage: 1,
_auditPerPage: 30,
async loadAuditLog(page) {
if (page) UI._auditPage = page;
const el = document.getElementById('adminAuditList');
el.innerHTML = '<div class="loading">Loading...</div>';
// Populate action filter on first load
const actionSel = document.getElementById('auditFilterAction');
if (actionSel && actionSel.options.length <= 1) {
try {
const resp = await API.adminListAuditActions();
(resp.actions || []).forEach(a => {
const opt = document.createElement('option');
opt.value = a;
opt.textContent = a;
actionSel.appendChild(opt);
});
} catch (e) { /* optional */ }
}
const params = {
page: UI._auditPage,
per_page: UI._auditPerPage,
action: document.getElementById('auditFilterAction')?.value || '',
resource_type: document.getElementById('auditFilterResource')?.value || '',
};
try {
const resp = await API.adminListAudit(params);
const entries = resp.data || [];
const total = resp.total || 0;
const totalPages = Math.ceil(total / UI._auditPerPage);
if (entries.length === 0) {
el.innerHTML = '<div class="empty-hint">No audit entries found</div>';
} else {
el.innerHTML = entries.map(e => {
const ts = new Date(e.created_at);
const timeStr = ts.toLocaleDateString() + ' ' + ts.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'});
const actor = e.actor_name || 'system';
const meta = UI._formatAuditMeta(e.metadata);
return `<div class="audit-row">
<div class="audit-main">
<span class="audit-action">${esc(e.action)}</span>
<span class="audit-actor">${esc(actor)}</span>
<span class="audit-resource">${esc(e.resource_type)}${e.resource_id ? ':' + esc(e.resource_id).slice(0,8) : ''}</span>
</div>
<div class="audit-meta">
${meta ? `<span class="audit-details">${meta}</span>` : ''}
<span class="audit-time">${timeStr}</span>
${e.ip_address ? `<span class="audit-ip">${esc(e.ip_address)}</span>` : ''}
</div>
</div>`;
}).join('');
}
// Pagination
const pgEl = document.getElementById('auditPagination');
if (totalPages > 1) {
pgEl.style.display = 'flex';
document.getElementById('auditPageInfo').textContent = `Page ${UI._auditPage} of ${totalPages} (${total} entries)`;
document.getElementById('auditPrevBtn').disabled = UI._auditPage <= 1;
document.getElementById('auditNextBtn').disabled = UI._auditPage >= totalPages;
} else {
pgEl.style.display = 'none';
}
} catch (e) { el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`; }
},
_formatAuditMeta(metaStr) {
try {
const m = typeof metaStr === 'string' ? JSON.parse(metaStr) : metaStr;
if (!m || Object.keys(m).length === 0) return '';
return Object.entries(m).map(([k,v]) => `${esc(k)}=${esc(String(v))}`).join(' · ');
} catch { return ''; }
},
async loadAdminProviders(quiet) {
const el = document.getElementById('adminProviderList');
const formEl = document.getElementById('adminAddProviderForm');
// Initialize admin provider form primitive (once)
if (!UI._adminProvForm && formEl) {
UI._adminProvForm = renderProviderForm(formEl, {
prefix: 'adminProv',
showPrivate: true,
showDefaultModel: true,
onSubmit: async (vals, isEdit) => {
try {
if (isEdit) {
const updates = {};
if (vals.name) updates.name = vals.name;
if (vals.endpoint) updates.endpoint = vals.endpoint;
if (vals.api_key) updates.api_key = vals.api_key;
updates.model_default = vals.model_default;
updates.is_private = vals.is_private || false;
await API.adminUpdateGlobalConfig(vals._editId, updates);
UI.toast('Provider updated', 'success');
} else {
if (!vals.name || !vals.endpoint || !vals.api_key) { UI.toast('Name, endpoint, and API key required', 'warning'); return; }
await API.adminCreateGlobalConfig(vals.name, vals.provider, vals.endpoint, vals.api_key, vals.model_default, vals.is_private || false);
UI.toast('Provider added', 'success');
}
formEl.style.display = 'none';
UI._adminProvForm.setCreateMode();
UI._adminProvList.refresh();
} catch (e) { UI.toast(e.message, 'error'); }
},
onCancel: () => {
formEl.style.display = 'none';
UI._adminProvForm.setCreateMode();
},
});
}
// Initialize admin provider list primitive (once)
if (!UI._adminProvList) {
UI._adminProvList = renderProviderList(el, {
apiFetch: () => API.adminListGlobalConfigs(),
showEndpoint: true,
showPrivate: true,
emptyMsg: 'No global providers — add one above',
onEdit: (prov) => {
if (UI._adminProvForm) {
UI._adminProvForm.setEditMode(prov.id, prov);
formEl.style.display = '';
}
},
onDelete: async (prov) => {
if (!await showConfirm('Delete this global provider?')) return;
try {
await API.adminDeleteGlobalConfig(prov.id);
UI.toast('Provider deleted', 'success');
UI._adminProvList.refresh();
} catch (e) { UI.toast(e.message, 'error'); }
},
});
}
await UI._adminProvList.refresh(quiet);
},
async loadAdminModels(quiet) {
const el = document.getElementById('adminModelList');
if (!quiet) el.innerHTML = '<div class="loading">Loading...</div>';
try {
const data = await API.adminListModels();
const list = data.models || data.data || data || [];
const arr = Array.isArray(list) ? list : [];
el.innerHTML = arr.map(m => {
const caps = m.capabilities || {};
const badges = renderCapBadges(caps, { compact: true });
// Support both old is_enabled (bool) and new visibility (string)
const vis = m.visibility || (m.is_enabled === true ? 'enabled' : m.is_enabled === false ? 'disabled' : 'disabled');
const visLabel = vis === 'enabled' ? '✓ Enabled' : vis === 'team' ? '👥 Team' : 'Disabled';
const visClass = vis === 'enabled' ? 'enabled' : vis === 'team' ? 'team' : '';
return `<div class="admin-model-row">
<span class="model-name">${esc(m.model_id || m.id)}</span>
<span class="model-caps-inline">${badges}</span>
<span class="provider-meta">${esc(m.provider_name || '')}</span>
<button class="admin-model-toggle ${visClass}" onclick="cycleModelVisibility('${m.id}', '${vis}')">${visLabel}</button>
</div>`;
}).join('') || '<div class="empty-hint">No models — add a provider first, then click Fetch Models</div>';
} catch (e) { el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`; }
},
async loadAdminPresets(quiet) {
const el = document.getElementById('adminPresetList');
if (!quiet) el.innerHTML = '<div class="loading">Loading...</div>';
try {
const data = await API.adminListPresets();
const list = data.presets || [];
// Ensure form is initialized for dropdown population
ensureAdminPresetForm();
// Populate model dropdown from admin model list (all synced models)
const modelSel = _adminPresetForm?.getModelSelect();
if (modelSel) {
modelSel.innerHTML = '<option value="">Select base model...</option>';
try {
const modelData = await API.adminListModels();
const allModels = modelData.models || modelData.data || [];
const arr = Array.isArray(allModels) ? allModels : [];
arr.forEach(m => {
const opt = document.createElement('option');
const mid = m.model_id || m.id;
opt.value = mid;
opt.textContent = mid + (m.provider_name ? ` (${m.provider_name})` : '');
modelSel.appendChild(opt);
});
} catch (e) { console.warn('Failed to load models for preset form:', e.message); }
}
// Populate config dropdown
const cfgSel = _adminPresetForm?.getConfigSelect();
if (cfgSel && cfgSel.options.length <= 1) {
try {
const cfgData = await API.adminListGlobalConfigs();
const cfgs = cfgData.configs || cfgData.data || [];
(Array.isArray(cfgs) ? cfgs : []).forEach(c => {
const opt = document.createElement('option');
opt.value = c.id;
opt.textContent = c.name + ' (' + c.provider + ')';
cfgSel.appendChild(opt);
});
} catch (e) { /* optional */ }
}
UI._presetCache = {};
el.innerHTML = list.map(p => {
UI._presetCache[p.id] = p;
const avatarEl = p.avatar
? `<img src="${p.avatar}" class="preset-row-avatar" alt="">`
: '';
// Scope badge with attribution
let scopeBadge = '';
if (p.scope === 'global') {
scopeBadge = '<span class="badge-admin">global</span>';
} else if (p.scope === 'team' && p.team_name) {
scopeBadge = `<span class="badge-team">👥 ${esc(p.team_name)}</span>`;
} else if (p.scope === 'personal') {
scopeBadge = '<span class="badge-user">personal</span>';
}
const creator = p.creator_name ? `<span class="text-muted" style="font-size:11px">by ${esc(p.creator_name)}</span>` : '';
const status = p.is_active ? '' : '<span class="badge-pending">inactive</span>';
return `<div class="admin-preset-row">
<div class="preset-info">
<strong>${avatarEl}${esc(p.name)}</strong> ${scopeBadge} ${creator} ${status}
<div class="preset-meta">${esc(p.base_model_id)} · ${esc(p.provider_name || 'auto')}</div>
${p.description ? `<div class="preset-desc">${esc(p.description)}</div>` : ''}
</div>
<div class="preset-actions">
<button class="btn-edit" onclick="editAdminPreset('${p.id}')" title="Edit">✎</button>
<button class="admin-model-toggle ${p.is_active ? 'enabled' : ''}" onclick="toggleAdminPreset('${p.id}', ${!p.is_active})">${p.is_active ? '✓ Active' : 'Inactive'}</button>
<button class="btn-delete" onclick="deleteAdminPreset('${p.id}', '${esc(p.name)}')" title="Delete">✕</button>
</div>
</div>`;
}).join('') || '<div class="empty-hint">No presets — create one to give users preconfigured model experiences</div>';
} catch (e) { el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`; }
},
// ── Teams Admin ─────────────────────────
_teamEditId: null,
async loadAdminTeams(quiet) {
const el = document.getElementById('adminTeamList');
const detail = document.getElementById('adminTeamDetail');
if (!quiet) {
el.innerHTML = '<div class="loading">Loading...</div>';
detail.style.display = 'none';
el.style.display = '';
document.getElementById('adminAddTeamForm').style.display = 'none';
}
try {
const resp = await API.adminListTeams();
const teams = resp.data || [];
el.innerHTML = teams.map(t => `
<div class="admin-user-row">
<div class="admin-user-info">
<div><strong>${esc(t.name)}</strong>
${t.is_active ? '' : '<span class="badge-pending">inactive</span>'}
</div>
<div class="text-muted">${esc(t.description || 'No description')} · ${t.member_count} member${t.member_count !== 1 ? 's' : ''}</div>
</div>
<div class="admin-user-actions">
<button class="btn-small" onclick="UI.openTeamDetail('${t.id}', '${esc(t.name)}')">Members</button>
<button class="admin-model-toggle ${t.is_active ? 'enabled' : ''}" onclick="toggleTeamActive('${t.id}', ${!t.is_active})">${t.is_active ? '✓ Active' : 'Inactive'}</button>
<button class="btn-delete" onclick="deleteTeam('${t.id}', '${esc(t.name)}')" title="Delete">✕</button>
</div>
</div>
`).join('') || '<div class="empty-hint">No teams yet — create one to organize users and set policies</div>';
} catch (e) { el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`; }
},
async openTeamDetail(teamId, teamName) {
this._teamEditId = teamId;
document.getElementById('adminTeamList').style.display = 'none';
document.getElementById('adminAddTeamForm').style.display = 'none';
const detail = document.getElementById('adminTeamDetail');
detail.style.display = '';
document.getElementById('adminTeamDetailName').textContent = teamName;
document.getElementById('adminAddMemberForm').style.display = 'none';
// Load team settings for policy checkboxes
try {
const team = await API.adminGetTeam(teamId);
const settings = typeof team.settings === 'string' ? JSON.parse(team.settings || '{}') : (team.settings || {});
document.getElementById('adminTeamPrivatePolicy').checked = !!settings.require_private_providers;
// allow_team_providers defaults to true if not explicitly set
document.getElementById('adminTeamAllowProviders').checked = settings.allow_team_providers !== false;
} catch (e) { /* proceed with defaults */ }
await this.loadTeamMembers(teamId);
},
async loadTeamMembers(teamId) {
const el = document.getElementById('adminMemberList');
el.innerHTML = '<div class="loading">Loading...</div>';
try {
const resp = await API.adminListMembers(teamId);
const members = resp.data || [];
el.innerHTML = members.map(m => `
<div class="admin-user-row">
<div class="admin-user-info">
<div><strong>${esc(m.display_name || m.email)}</strong>
<span class="badge-${m.role === 'admin' ? 'admin' : 'user'}">${m.role}</span>
${m.user_role === 'admin' ? '<span class="badge-admin">sys-admin</span>' : ''}
</div>
<div class="text-muted">${esc(m.email)}</div>
</div>
<div class="admin-user-actions">
<select onchange="updateTeamMember('${teamId}', '${m.id}', this.value)" class="inline-select">
<option value="member" ${m.role === 'member' ? 'selected' : ''}>Member</option>
<option value="admin" ${m.role === 'admin' ? 'selected' : ''}>Team Admin</option>
</select>
<button class="btn-delete" onclick="removeTeamMember('${teamId}', '${m.id}', '${esc(m.email)}')" title="Remove">✕</button>
</div>
</div>
`).join('') || '<div class="empty-hint">No members — add users to this team</div>';
} catch (e) { el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`; }
},
async loadMemberUserDropdown(teamId) {
const sel = document.getElementById('adminMemberUser');
sel.innerHTML = '<option value="">Loading...</option>';
try {
const resp = await API.adminListUsers(1, 200);
const users = resp.users || resp.data || [];
// Also get existing members to exclude
const memberResp = await API.adminListMembers(teamId);
const existingIds = new Set((memberResp.data || []).map(m => m.user_id));
const available = users.filter(u => u.is_active && !existingIds.has(u.id));
sel.innerHTML = '<option value="">Select user...</option>' +
available.map(u => `<option value="${u.id}">${esc(u.username || u.email)}</option>`).join('');
} catch (e) { sel.innerHTML = `<option value="">Error loading users</option>`; }
},
async loadAdminSettings() {
try {
const data = await API.adminGetSettings();
// v0.9: { settings: { banner: {...}, ... }, policies: { allow_registration: "true", ... } }
const settings = data.settings || {};
const policies = data.policies || {};
// Helper to read from settings map (JSONB values)
const getSetting = (key, fallback) => {
const v = settings[key];
if (v === undefined || v === null) return fallback;
// Unwrap {value: X} wrapper if present
return (v && typeof v === 'object' && 'value' in v) ? v.value : v;
};
// Registration (policy: allow_registration)
document.getElementById('adminRegToggle').checked = policies.allow_registration === 'true';
// Admin system prompt (global_settings)
const sysPrompt = getSetting('system_prompt', {}) || {};
document.getElementById('adminSystemPrompt').value = sysPrompt.content || '';
// Registration default state (policy: default_user_active → 'true' means auto-active)
const defaultActive = policies.default_user_active === 'true';
document.getElementById('adminRegDefaultState').value = defaultActive ? 'active' : 'pending';
// User providers / BYOK (policy: allow_user_byok)
document.getElementById('adminUserProvidersToggle').checked = policies.allow_user_byok === 'true';
// User presets / personas (policy: allow_user_personas)
document.getElementById('adminUserPresetsToggle').checked = policies.allow_user_personas === 'true';
// Default model (policy: default_model) — populate from current model list
const defModelSel = document.getElementById('adminDefaultModel');
if (defModelSel) {
defModelSel.innerHTML = '<option value="">— None (first visible) —</option>';
App.models.filter(m => !m.hidden).forEach(m => {
const opt = document.createElement('option');
opt.value = m.baseModelId || m.id;
opt.textContent = (m.name || m.id) + (m.provider ? ` (${m.provider})` : '');
defModelSel.appendChild(opt);
});
defModelSel.value = policies.default_model || '';
}
// Banner (global_settings)
const banner = getSetting('banner', {}) || {};
document.getElementById('adminBannerEnabled').checked = !!banner.enabled;
document.getElementById('adminBannerText').value = banner.text || '';
document.getElementById('adminBannerPosition').value = banner.position || 'both';
document.getElementById('adminBannerBg').value = banner.bg || '#007a33';
document.getElementById('adminBannerBgHex').value = banner.bg || '#007a33';
document.getElementById('adminBannerFg').value = banner.fg || '#ffffff';
document.getElementById('adminBannerFgHex').value = banner.fg || '#ffffff';
document.getElementById('bannerConfigFields').style.display = banner.enabled ? '' : 'none';
UI.updateBannerPreview();
// Load banner presets (global_settings)
const presets = getSetting('banner_presets', {}) || {};
const sel = document.getElementById('adminBannerPreset');
sel.innerHTML = '<option value="">Custom</option>';
Object.entries(presets).forEach(([key, p]) => {
sel.innerHTML += `<option value="${esc(key)}">${esc(p.text || key)}</option>`;
});
UI._bannerPresets = presets;
// Vault / Encryption status
const vaultEl = document.getElementById('adminVaultStatus');
if (vaultEl) {
try {
const vault = await API.adminGetVaultStatus();
const keyBadge = vault.encryption_key_set
? '<span class="badge badge-success">Active</span>'
: '<span class="badge badge-warning">Not Set</span>';
vaultEl.innerHTML = `
<div class="admin-storage-grid" style="margin-top:6px">
<div class="admin-storage-item">
<span class="admin-storage-label">Encryption</span>
${keyBadge}
</div>
<div class="admin-storage-item">
<span class="admin-storage-label">Encrypted Keys</span>
<span class="admin-storage-value">${vault.encrypted_keys}</span>
</div>
<div class="admin-storage-item">
<span class="admin-storage-label">Vault Users</span>
<span class="admin-storage-value">${vault.vault_users}</span>
</div>
</div>`;
} catch (e) {
vaultEl.innerHTML = '<span class="empty-hint">Could not load vault status</span>';
}
}
} catch (e) { console.debug('Failed to load admin settings:', e); }
},
_bannerPresets: {},
updateBannerPreview() {
const prev = document.getElementById('bannerPreview');
if (!prev) return;
const text = document.getElementById('adminBannerText').value || 'PREVIEW';
const bg = document.getElementById('adminBannerBg').value;
const fg = document.getElementById('adminBannerFg').value;
prev.textContent = text;
prev.style.background = bg;
prev.style.color = fg;
},
});