// ==========================================
// Chat Switchboard – UI Admin
// ==========================================
// Fullscreen admin panel with category + section navigation.
// All loadAdmin*() functions unchanged — same endpoints, same DOM IDs.
// ── Category → Section mapping ────────────
const ADMIN_SECTIONS = {
people: ['users', 'teams', 'groups'],
ai: ['providers', 'models', 'presets', 'roles', 'knowledgeBases', 'memory'],
system: ['settings', 'storage', 'extensions'],
monitoring: ['usage', 'audit', 'stats'],
};
const ADMIN_LABELS = {
users: 'Users', teams: 'Teams', groups: 'Groups',
providers: 'Providers', models: 'Models', presets: 'Personas', roles: 'Roles', knowledgeBases: 'Knowledge', memory: 'Memory',
settings: 'Settings', storage: 'Storage', extensions: 'Extensions',
usage: 'Usage', audit: 'Audit', stats: 'Stats',
};
// Section → loader mapping (reuses all existing loadAdmin* functions)
const ADMIN_LOADERS = {
users: () => UI.loadAdminUsers(),
teams: () => UI.loadAdminTeams(),
groups: () => UI.loadAdminGroups(),
roles: () => UI.loadAdminRoles(),
providers: () => UI.loadAdminProviders(),
models: () => UI.loadAdminModels(),
presets: () => UI.loadAdminPresets(),
knowledgeBases: () => {
if (typeof KnowledgeUI === 'undefined') { console.error('[Admin] KnowledgeUI not loaded — check js/knowledge-ui.js'); return; }
KnowledgeUI.openAdminPanel();
},
memory: () => {
if (typeof MemoryUI !== 'undefined') { MemoryUI.openAdminPanel(); }
else { console.error('[Admin] MemoryUI not loaded — check js/memory-ui.js'); }
},
settings: () => UI.loadAdminSettings(),
storage: () => typeof loadAdminStorage === 'function' ? loadAdminStorage() : null,
extensions: () => UI.loadAdminExtensions(),
usage: () => UI.loadAdminUsage(),
audit: () => UI.loadAuditLog(),
stats: () => UI.loadAdminStats(),
};
// Find which category a section belongs to
function _adminCatForSection(section) {
for (const [cat, sections] of Object.entries(ADMIN_SECTIONS)) {
if (sections.includes(section)) return cat;
}
return 'people';
}
Object.assign(UI, {
// ── Admin Panel State ─────────────────────
_adminCat: 'people',
_adminSection: 'users',
// ── Open / Close ──────────────────────────
openAdmin(cat, section) {
UI._adminCat = cat || 'people';
UI._adminSection = section || ADMIN_SECTIONS[UI._adminCat][0];
const panel = document.getElementById('adminPanel');
panel.style.display = 'flex';
// Show current user email
const userEl = document.getElementById('adminCurrentUser');
if (userEl && API.user) userEl.textContent = API.user.email || API.user.username || '';
UI._renderAdminNav();
UI._switchAdminSection(UI._adminSection);
},
closeAdmin() {
document.getElementById('adminPanel').style.display = 'none';
},
// ── Navigate to specific section ──────────
// Called from command palette, deep links, etc.
openAdminSection(section) {
const cat = _adminCatForSection(section);
UI.openAdmin(cat, section);
},
// ── Category switch ───────────────────────
switchAdminCategory(cat) {
UI._adminCat = cat;
UI._adminSection = ADMIN_SECTIONS[cat][0];
UI._renderAdminNav();
UI._switchAdminSection(UI._adminSection);
},
// ── Render navigation state ───────────────
_renderAdminNav() {
// Highlight active category
document.querySelectorAll('.admin-cat').forEach(btn => {
btn.classList.toggle('active', btn.dataset.cat === UI._adminCat);
});
// Populate sidebar
const sidebar = document.getElementById('adminSidebar');
sidebar.innerHTML = ADMIN_SECTIONS[UI._adminCat].map(sec =>
``
).join('');
// Wire sidebar clicks
sidebar.querySelectorAll('button').forEach(btn => {
btn.addEventListener('click', () => UI._switchAdminSection(btn.dataset.section));
});
},
// ── Section switch (shows content + lazy-loads) ──
async _switchAdminSection(section) {
UI._adminSection = section;
// Update sidebar highlight
document.querySelectorAll('.admin-sidebar button').forEach(btn => {
btn.classList.toggle('active', btn.dataset.section === section);
});
// Hide all, show target
document.querySelectorAll('.admin-section-content').forEach(c => c.style.display = 'none');
const panel = document.getElementById(`admin${section.charAt(0).toUpperCase() + section.slice(1)}Tab`);
if (panel) panel.style.display = '';
// Lazy-load data
await ADMIN_LOADERS[section]?.();
},
// ── Backward compat: switchAdminTab still works ──
// (called by some inline onclick handlers and admin-handlers.js)
async switchAdminTab(tab) {
const cat = _adminCatForSection(tab);
if (document.getElementById('adminPanel').style.display !== 'flex') {
UI.openAdmin(cat, tab);
} else {
if (cat !== UI._adminCat) {
UI._adminCat = cat;
UI._renderAdminNav();
}
await UI._switchAdminSection(tab);
}
},
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 => `
${esc(t.name)}
admin
Manage →
${t.description ? `
${esc(t.description)}
` : ''}
${t.member_count} member${t.member_count !== 1 ? 's' : ''}
`).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);
if (tab === 'knowledge') {
if (typeof KnowledgeUI === 'undefined') {
console.error('[TeamAdmin] KnowledgeUI not loaded — check js/knowledge-ui.js');
const c = document.getElementById('teamKBContent');
if (c) c.innerHTML = 'Knowledge UI failed to load. Check browser console.
';
} else {
KnowledgeUI.openTeamPanel(teamId);
}
}
if (tab === 'groups') UI.loadTeamManageGroups(teamId);
},
async loadAdminUsers(quiet) {
const el = document.getElementById('adminUserList');
if (!quiet) el.innerHTML = 'Loading...
';
try {
const resp = await API.adminListUsers();
const users = resp.users || resp.data || [];
el.innerHTML = users.map(u => {
const teamBadges = (u.teams || []).map(t =>
`${esc(t.team_name)}`
).join(' ');
const isPending = !u.is_active;
return `
${esc(u.username)} ${u.role}
${isPending ? 'pending' : ''}
${teamBadges}
${esc(u.email)}
${isPending
? ``
: ``
}
`;
}).join('') || 'No users
';
} catch (e) { el.innerHTML = `${esc(e.message)}
`; }
},
async loadAdminStats() {
const el = document.getElementById('adminStats');
el.innerHTML = 'Loading...
';
try {
const s = await API.adminGetStats();
const labels = { users: 'Users', channels: 'Channels', messages: 'Messages' };
el.innerHTML = '' +
Object.entries(s).map(([k, v]) => `
${typeof v === 'number' ? v.toLocaleString() : esc(String(v))}
${labels[k] || k.replace(/_/g, ' ')}
`).join('') +
'
';
} catch (e) { el.innerHTML = `${esc(e.message)}
`; }
},
// ── 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 = 'No pricing configured. Sync providers to populate from catalog.
';
} else {
pricingEl.innerHTML = `
| Model |
Input $/M |
Output $/M |
Source |
${entries.map(p => `
| ${esc(p.model_id)} |
${p.input_per_m != null ? '$' + Number(p.input_per_m).toFixed(4) : '—'} |
${p.output_per_m != null ? '$' + Number(p.output_per_m).toFixed(4) : '—'} |
${p.source} |
`).join('')}
`;
}
} catch (e) { pricingEl.innerHTML = `${esc(e.message)}
`; }
}
},
async loadAdminExtensions() {
const el = document.getElementById('adminExtensionsList');
el.innerHTML = 'Loading...
';
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 = 'No extensions installed
';
return;
}
el.innerHTML = exts.map(ext => {
const statusBadge = ext.is_enabled
? 'enabled'
: 'disabled';
const systemBadge = ext.is_system
? ' system'
: '';
return `
${esc(ext.name)}
${esc(ext.ext_id)}
${statusBadge}${systemBadge}
${esc(ext.description || 'No description')}
${ext.author ? ` · by ${esc(ext.author)}` : ''}
· v${esc(ext.version)} · ${ext.tier}
`;
}).join('');
} catch (e) {
el.innerHTML = 'Failed to load extensions
';
}
},
_auditPage: 1,
_auditPerPage: 30,
async loadAuditLog(page) {
if (page) UI._auditPage = page;
const el = document.getElementById('adminAuditList');
el.innerHTML = 'Loading...
';
// 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 = 'No audit entries found
';
} 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 `
${esc(e.action)}
${esc(actor)}
${esc(e.resource_type)}${e.resource_id ? ':' + esc(e.resource_id).slice(0,8) : ''}
${meta ? `${meta}` : ''}
${timeStr}
${e.ip_address ? `${esc(e.ip_address)}` : ''}
`;
}).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 = `${esc(e.message)}
`; }
},
_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 = 'Loading...
';
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 `
${esc(m.model_id || m.id)}
${badges}
${esc(m.provider_name || '')}
`;
}).join('') || 'No models — add a provider first, then click Fetch Models
';
} catch (e) { el.innerHTML = `${esc(e.message)}
`; }
},
async loadAdminPresets(quiet) {
const el = document.getElementById('adminPresetList');
if (!quiet) el.innerHTML = 'Loading...
';
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 = '';
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
? `
`
: '';
// Scope badge with attribution
let scopeBadge = '';
if (p.scope === 'global') {
scopeBadge = 'global';
} else if (p.scope === 'team' && p.team_name) {
scopeBadge = `👥 ${esc(p.team_name)}`;
} else if (p.scope === 'personal') {
scopeBadge = 'personal';
}
const creator = p.creator_name ? `by ${esc(p.creator_name)}` : '';
const status = p.is_active ? '' : 'inactive';
return `
${avatarEl}${esc(p.name)} ${scopeBadge} ${creator} ${status}
${esc(p.base_model_id)} · ${esc(p.provider_name || 'auto')}
${p.description ? `
${esc(p.description)}
` : ''}
`;
}).join('') || 'No presets — create one to give users preconfigured model experiences
';
} catch (e) { el.innerHTML = `${esc(e.message)}
`; }
},
// ── Teams Admin ─────────────────────────
_teamEditId: null,
async loadAdminTeams(quiet) {
const el = document.getElementById('adminTeamList');
const detail = document.getElementById('adminTeamDetail');
if (!quiet) {
el.innerHTML = 'Loading...
';
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 => `
${esc(t.name)}
${t.is_active ? '' : 'inactive'}
${esc(t.description || 'No description')} · ${t.member_count} member${t.member_count !== 1 ? 's' : ''}
`).join('') || 'No teams yet — create one to organize users and set policies
';
} catch (e) { el.innerHTML = `${esc(e.message)}
`; }
},
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 = 'Loading...
';
try {
const resp = await API.adminListMembers(teamId);
const members = resp.data || [];
el.innerHTML = members.map(m => `
${esc(m.display_name || m.email)}
${m.role}
${m.user_role === 'admin' ? 'sys-admin' : ''}
${esc(m.email)}
`).join('') || 'No members — add users to this team
';
} catch (e) { el.innerHTML = `${esc(e.message)}
`; }
},
async loadMemberUserDropdown(teamId) {
const sel = document.getElementById('adminMemberUser');
sel.innerHTML = '';
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 = '' +
available.map(u => ``).join('');
} catch (e) { sel.innerHTML = ``; }
},
// ── Groups Admin ────────────────────────
_groupEditId: null,
async loadAdminGroups(quiet) {
const el = document.getElementById('adminGroupList');
const detail = document.getElementById('adminGroupDetail');
if (!quiet) {
el.innerHTML = 'Loading...
';
detail.style.display = 'none';
el.style.display = '';
document.getElementById('adminAddGroupForm').style.display = 'none';
}
try {
const resp = await API.adminListGroups();
const groups = resp.data || [];
el.innerHTML = groups.map(g => {
const scopeBadge = g.scope === 'global'
? 'global'
: `team`;
return `
${esc(g.name)} ${scopeBadge}
${esc(g.description || 'No description')} · ${g.member_count} member${g.member_count !== 1 ? 's' : ''}
`;
}).join('') || 'No groups yet — create one to control access to personas and knowledge bases
';
} catch (e) { el.innerHTML = `${esc(e.message)}
`; }
},
async openGroupDetail(groupId, groupName) {
this._groupEditId = groupId;
document.getElementById('adminGroupList').style.display = 'none';
document.getElementById('adminAddGroupForm').style.display = 'none';
const detail = document.getElementById('adminGroupDetail');
detail.style.display = '';
document.getElementById('adminGroupDetailName').textContent = groupName;
document.getElementById('adminAddGroupMemberForm').style.display = 'none';
// Load group details for meta line
try {
const g = await API.adminGetGroup(groupId);
const scope = g.scope === 'global' ? 'Global group' : 'Team-scoped group';
document.getElementById('adminGroupDetailMeta').textContent = `${scope} · ${g.member_count} member${g.member_count !== 1 ? 's' : ''}`;
} catch (e) { /* proceed */ }
await this.loadGroupMembers(groupId);
},
async loadGroupMembers(groupId) {
const el = document.getElementById('adminGroupMemberList');
el.innerHTML = 'Loading...
';
try {
const resp = await API.adminListGroupMembers(groupId);
const members = resp.data || [];
el.innerHTML = members.map(m => `
${esc(m.display_name || m.username || m.email)}
${esc(m.email)} · added ${new Date(m.added_at).toLocaleDateString()}
`).join('') || 'No members — add users to this group
';
} catch (e) { el.innerHTML = `${esc(e.message)}
`; }
},
async loadGroupMemberDropdown(groupId) {
const sel = document.getElementById('adminGroupMemberUser');
sel.innerHTML = '';
try {
const resp = await API.adminListUsers(1, 200);
const users = resp.users || resp.data || [];
// Exclude existing members
const memberResp = await API.adminListGroupMembers(groupId);
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 = '' +
available.map(u => ``).join('');
} catch (e) { sel.innerHTML = ``; }
},
// ── Grant Picker (shared for Personas + KBs) ──
async openGrantPicker(resourceType, resourceId, resourceName, anchorSelector) {
// Fetch current grant + all groups for the picker
const [grantResp, groupsResp] = await Promise.all([
API.adminGetGrant(resourceType, resourceId),
API.adminListGroups(),
]);
const grant = grantResp;
const groups = (groupsResp.data || []);
const currentScope = grant.grant_scope || 'team_only';
const currentGroups = new Set(grant.granted_groups || []);
const html = ``;
// Remove any existing picker, then insert after the anchor row
document.querySelectorAll('.grant-picker-active').forEach(el => el.remove());
const row = document.querySelector(anchorSelector);
if (row) {
const wrapper = document.createElement('div');
wrapper.className = 'grant-picker-active';
wrapper.innerHTML = html;
row.after(wrapper);
// Wire scope toggle
const scopeSel = document.getElementById(`grantScope_${resourceId}`);
const groupsDiv = document.getElementById(`grantGroups_${resourceId}`);
scopeSel?.addEventListener('change', () => {
groupsDiv.style.display = scopeSel.value === 'groups' ? '' : 'none';
});
}
},
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';
// KB direct access (policy: kb_direct_access — v0.17.0)
const kbDirectEl = document.getElementById('adminKBDirectAccessToggle');
if (kbDirectEl) kbDirectEl.checked = policies.kb_direct_access === 'true';
// Default model (policy: default_model) — populate from current model list
const defModelSel = document.getElementById('adminDefaultModel');
if (defModelSel) {
defModelSel.innerHTML = '';
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 = '';
Object.entries(presets).forEach(([key, p]) => {
sel.innerHTML += ``;
});
UI._bannerPresets = presets;
// Vault / Encryption status
const vaultEl = document.getElementById('adminVaultStatus');
// Web Search config (global_settings)
const searchCfg = getSetting('search_config', {}) || {};
document.getElementById('adminSearchProvider').value = searchCfg.provider || 'duckduckgo';
document.getElementById('adminSearchEndpoint').value = searchCfg.endpoint || '';
document.getElementById('adminSearchApiKey').value = searchCfg.api_key || '';
document.getElementById('adminSearchMaxResults').value = String(searchCfg.max_results || 5);
document.getElementById('searxngConfigFields').style.display =
(searchCfg.provider === 'searxng') ? '' : 'none';
// Auto-Compaction (global_settings)
const compactionCfg = getSetting('auto_compaction', {}) || {};
const compactionEnabled = document.getElementById('adminCompactionEnabled');
if (compactionEnabled) {
compactionEnabled.checked = !!compactionCfg.enabled;
document.getElementById('compactionConfigFields').style.display =
compactionCfg.enabled ? '' : 'none';
}
const compThreshold = document.getElementById('adminCompactionThreshold');
if (compThreshold) compThreshold.value = String(compactionCfg.threshold || 70);
const compCooldown = document.getElementById('adminCompactionCooldown');
if (compCooldown) compCooldown.value = String(compactionCfg.cooldown || 30);
// Memory extraction config (v0.18.0)
const memExtractionCfg = getSetting('memory_extraction', {}) || {};
const memEnabled = document.getElementById('adminMemoryExtractionEnabled');
if (memEnabled) {
memEnabled.checked = !!memExtractionCfg.enabled;
const memFields = document.getElementById('memoryExtractionConfigFields');
if (memFields) memFields.style.display = memExtractionCfg.enabled ? '' : 'none';
memEnabled.addEventListener('change', () => {
if (memFields) memFields.style.display = memEnabled.checked ? '' : 'none';
});
}
const memAutoApprove = document.getElementById('adminMemoryAutoApprove');
if (memAutoApprove) memAutoApprove.checked = !!memExtractionCfg.auto_approve;
// Email / SMTP config (v0.20.0 Phase 3)
const notifCfg = getSetting('notifications', {}) || {};
if (typeof NotifPrefs !== 'undefined') {
NotifPrefs._loadAdminSmtp(notifCfg);
NotifPrefs._initAdminSmtp();
}
if (vaultEl) {
try {
const vault = await API.adminGetVaultStatus();
const keyBadge = vault.encryption_key_set
? 'Active'
: 'Not Set';
vaultEl.innerHTML = `
Encryption
${keyBadge}
Encrypted Keys
${vault.encrypted_keys}
Vault Users
${vault.vault_users}
`;
} catch (e) {
vaultEl.innerHTML = 'Could not load vault status';
}
}
} 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;
},
});