// ==========================================
// 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', 'personas', 'roles', 'knowledgeBases', 'memory'],
routing: ['health', 'routing', 'capabilities'],
system: ['settings', 'storage', 'extensions'],
monitoring: ['usage', 'audit', 'stats'],
};
const ADMIN_LABELS = {
users: 'Users', teams: 'Teams', groups: 'Groups',
providers: 'Providers', models: 'Models', personas: 'Personas', roles: 'Roles', knowledgeBases: 'Knowledge', memory: 'Memory',
health: 'Health', routing: 'Routing', capabilities: 'Capabilities',
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(),
personas: () => UI.loadAdminPersonas(),
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(),
health: () => UI.loadAdminHealth(),
routing: () => UI.loadAdminRouting(),
capabilities: () => UI.loadAdminCapabilities(),
usage: () => UI.loadAdminUsage(),
audit: () => UI.loadAuditLog(),
stats: () => UI.loadAdminStats(),
channels: () => UI.loadAdminChannels(),
};
// 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 === 'personas') UI.loadTeamManagePersonas(teamId);
if (tab === 'usage') UI.loadTeamUsage();
if (tab === 'activity') UI.loadTeamAuditLog(1);
if (tab === 'settings') UI.loadTeamManageSettings(teamId);
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 || [];
if (!users.length) { el.innerHTML = 'No users
'; return; }
const rows = users.map(u => {
const initial = (u.display_name || u.username || '?')[0].toUpperCase();
const name = esc(u.display_name || u.username);
const handle = '@' + esc(u.username);
const roleCls = u.role === 'admin' ? 'badge-admin' : u.role === 'service' ? 'badge-service' : 'badge-user';
const isPending = !u.is_active;
const statusCls = isPending ? 'badge-warning' : 'badge-active';
const statusLabel = isPending ? 'pending' : 'active';
const teams = (u.teams || []).map(t => esc(t.team_name)).join(', ') || '\u2014';
const lastSeen = u.last_login_at ? _relativeTime(u.last_login_at) : '\u2014';
return `
|
${u.role} |
${statusLabel} |
${teams} |
${lastSeen} |
${isPending
? ``
: ``
}
|
`;
}).join('');
el.innerHTML = `
| User | Role | Status | Teams | Last Seen | |
${rows}
`;
// Approve form placeholder (injected per-user when needed)
} 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', models: 'Models', providers: 'Providers', personas: 'Personas' };
el.innerHTML = Object.entries(s).map(([k, v]) => `
${labels[k] || k.replace(/_/g, ' ')}
${typeof v === 'number' ? v.toLocaleString() : esc(String(v))}
`).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() {
// Stat cards + bar chart
const cardsEl = document.getElementById('adminUsageCards');
const chartEl = document.getElementById('adminUsageChart');
// Fetch 7-day usage for cards and chart
if (cardsEl) {
try {
const weekData = await API.adminGetUsage({ period: '7d', group_by: 'day' });
const buckets = weekData.data || weekData.buckets || [];
const totalReqs = buckets.reduce((s, b) => s + (b.request_count || 0), 0);
const totalIn = buckets.reduce((s, b) => s + (b.input_tokens || 0), 0);
const totalOut = buckets.reduce((s, b) => s + (b.output_tokens || 0), 0);
const totalTokens = totalIn + totalOut;
const tokenStr = totalTokens > 1e6 ? (totalTokens / 1e6).toFixed(1) + 'M' : totalTokens > 1e3 ? (totalTokens / 1e3).toFixed(0) + 'K' : totalTokens;
const userSet = new Set(buckets.filter(b => b.user_id).map(b => b.user_id));
const avgMs = buckets.reduce((s, b) => s + (b.avg_latency_ms || 0), 0);
const avgLatency = buckets.length ? (avgMs / buckets.length / 1000).toFixed(1) + 's' : '—';
cardsEl.innerHTML = `
Total Requests
${totalReqs.toLocaleString()}
Last 7 days
Tokens Used
${tokenStr}
Input + output
Active Users
${userSet.size || '—'}
Last 7 days
Avg Response
${avgLatency}
p50 latency
`;
// Bar chart — aggregate by day label
if (chartEl && buckets.length > 0) {
const dayMap = {};
buckets.forEach(b => {
const d = new Date(b.window_start || b.date || b.bucket);
const label = d.toLocaleDateString('en', { weekday: 'short' });
dayMap[label] = (dayMap[label] || 0) + (b.request_count || 0);
});
const days = Object.entries(dayMap);
const maxVal = Math.max(...days.map(d => d[1]), 1);
chartEl.innerHTML = `
Requests per Day
${days.map(([label, val]) => {
const pct = Math.round(val / maxVal * 100);
return `
`;
}).join('')}
`;
}
} catch (e) {
cardsEl.innerHTML = '';
if (chartEl) chartEl.innerHTML = '';
}
}
// Usage breakdown via primitive (preserves existing period/group-by controls)
const totalsEl = document.getElementById('adminUsageTotals');
if (!totalsEl) return;
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
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;
if (exts.length === 0) {
el.innerHTML = 'No extensions installed
';
return;
}
el.innerHTML = `
| Extension | Status | Tier | Version | |
${exts.map(ext => {
const statusBadge = ext.is_enabled ? 'enabled' : 'disabled';
const systemBadge = ext.is_system ? ' system' : '';
return `
|
${esc(ext.name)}${systemBadge}
${esc(ext.description || 'No description')}${ext.author ? ' · by ' + esc(ext.author) : ''}
|
${statusBadge} |
${esc(ext.tier)} |
v${esc(ext.version)} |
|
`;
}).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 = `
| Action | Actor | Resource | Details | Time | IP |
${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 || '—'} |
${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) — now renders inside modal body
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');
}
closeModal('providerFormModal');
UI._adminProvForm.setCreateMode();
UI.loadAdminProviders(true);
} catch (e) { UI.toast(e.message, 'error'); }
},
onCancel: () => {
closeModal('providerFormModal');
UI._adminProvForm.setCreateMode();
},
});
}
if (!quiet) el.innerHTML = 'Loading...
';
try {
// Fetch providers, health, and models in parallel
const [configsRaw, healthRaw, modelsRaw] = await Promise.all([
API.adminListGlobalConfigs(),
API.adminGetAllProviderHealth().catch(() => ({ providers: [] })),
API.adminListModels().catch(() => ({ models: [] })),
]);
const configs = Array.isArray(configsRaw) ? configsRaw : (configsRaw.configs || configsRaw.providers || configsRaw.data || []);
const healthList = healthRaw.providers || healthRaw.data || healthRaw || [];
const models = modelsRaw.models || modelsRaw.data || modelsRaw || [];
// Build health lookup by provider_config_id
const healthMap = {};
(Array.isArray(healthList) ? healthList : []).forEach(h => { healthMap[h.provider_config_id] = h; });
// Count models per provider
const modelCounts = {};
(Array.isArray(models) ? models : []).forEach(m => {
const pid = m.provider_config_id;
if (pid) modelCounts[pid] = (modelCounts[pid] || 0) + 1;
});
// Cache for edit
UI._providerCache = {};
configs.forEach(c => { UI._providerCache[c.id] = c; });
if (!configs.length) {
el.innerHTML = 'No providers configured \u2014 click + Add above
';
return;
}
el.innerHTML = configs.map(c => {
const health = healthMap[c.id];
const status = health ? health.status : 'unknown';
const statusCls = status === 'healthy' ? 'badge-healthy'
: status === 'degraded' ? 'badge-degraded'
: status === 'unhealthy' ? 'badge-unhealthy'
: 'badge-unknown';
const count = modelCounts[c.id] || 0;
const scope = c.is_private ? 'Private' : (c.team_id ? 'Team' : '');
return `
${esc(c.provider || '')}
${count ? `${count} model${count !== 1 ? 's' : ''}` : ''}
`;
}).join('');
} catch (e) { el.innerHTML = `${esc(e.message)}
`; }
// Delegate card clicks → open edit modal
if (!el._delegated) {
el._delegated = true;
el.addEventListener('click', (e) => {
const card = e.target.closest('.provider-card');
if (!card) return;
const id = card.dataset.id;
const prov = UI._providerCache?.[id];
if (!prov || !UI._adminProvForm) return;
UI._adminProvForm.setEditMode(prov.id, prov);
document.getElementById('providerFormTitle').textContent = 'Edit Provider';
openModal('providerFormModal');
});
}
},
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 : [];
if (!arr.length) { el.innerHTML = 'No models — add a provider first, then click Fetch Models
'; return; }
el.innerHTML = `
| Model | Provider | Capabilities | Visibility |
${arr.map(m => {
const caps = m.capabilities || {};
const badges = renderCapBadges(caps, { compact: true });
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)} |
${esc(m.provider_name || '')} |
${badges} |
|
`;
}).join('')}
`;
} catch (e) { el.innerHTML = `${esc(e.message)}
`; }
},
async loadAdminPersonas(quiet) {
const el = document.getElementById('adminPersonaList');
if (!quiet) el.innerHTML = 'Loading...
';
try {
const data = await API.adminListPersonas();
const list = data.personas || [];
// Ensure form is initialized for dropdown population
ensureAdminPersonaForm();
// Populate model dropdown from admin model list (all synced models)
const modelSel = _adminPersonaForm?.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 persona form:', e.message); }
}
// Populate config dropdown
const cfgSel = _adminPersonaForm?.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._personaCache = {};
if (!list.length) { el.innerHTML = 'No personas — create one to give users preconfigured model experiences
'; return; }
el.innerHTML = `
| Persona | Scope | Model | Status | |
${list.map(p => {
UI._personaCache[p.id] = p;
const avatarEl = p.avatar
? `
`
: '';
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)}
` : '';
return `
${avatarEl}
${esc(p.name)}
${p.description ? ` ${esc(p.description)} ` : creator}
|
${scopeBadge} |
${esc(p.base_model_id)} |
${p.is_active ? 'active' : 'inactive'} |
|
`;
}).join('')}
`;
} 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 = '';
closeModal('createTeamModal');
}
try {
const resp = await API.adminListTeams();
const teams = resp.data || [];
if (!teams.length) { el.innerHTML = 'No teams yet — create one to organize users and set policies
'; return; }
el.innerHTML = `
| Team | Status | Members | |
${teams.map(t => `
|
${esc(t.name)}
${esc(t.description || 'No description')}
|
${t.is_active ? 'active' : 'inactive'}
|
${t.member_count} member${t.member_count !== 1 ? 's' : ''} |
|
`).join('')}
`;
} catch (e) { el.innerHTML = `${esc(e.message)}
`; }
},
async openTeamDetail(teamId, teamName) {
this._teamEditId = teamId;
document.getElementById('adminTeamList').style.display = 'none';
closeModal('createTeamModal');
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 || [];
if (!members.length) { el.innerHTML = 'No members — add users to this team
'; return; }
el.innerHTML = `
| Member | Role | |
${members.map(m => `
|
${esc(m.display_name || m.email)}
${esc(m.email)}
|
${m.user_role === 'admin' ? 'sys-admin' : ''}
|
|
`).join('')}
`;
} 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 = '';
closeModal('createGroupModal');
}
try {
const resp = await API.adminListGroups();
const groups = resp.data || [];
if (!groups.length) { el.innerHTML = 'No groups yet — create one to control access to personas and knowledge bases
'; return; }
el.innerHTML = `
| Group | Scope | Members | |
${groups.map(g => {
const scopeBadge = g.scope === 'global'
? 'global'
: 'team';
return `
|
${esc(g.name)}
${esc(g.description || 'No description')}
|
${scopeBadge} |
${g.member_count} member${g.member_count !== 1 ? 's' : ''} |
|
`;
}).join('')}
`;
} catch (e) { el.innerHTML = `${esc(e.message)}
`; }
},
async openGroupDetail(groupId, groupName) {
this._groupEditId = groupId;
document.getElementById('adminGroupList').style.display = 'none';
closeModal('createGroupModal');
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 || [];
if (!members.length) { el.innerHTML = 'No members — add users to this group
'; return; }
el.innerHTML = `
| Member | Added | |
${members.map(m => `
|
${esc(m.display_name || m.username || m.email)}
${esc(m.email)}
|
${new Date(m.added_at).toLocaleDateString()} |
|
`).join('')}
`;
} 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';
});
}
},
// ── Provider Health Dashboard (v0.22.3) ──────
async loadAdminHealth() {
const cardsEl = document.getElementById('adminHealthCards');
const el = document.getElementById('adminHealthContent');
if (cardsEl) cardsEl.innerHTML = '';
el.innerHTML = 'Loading health data...
';
try {
const data = await API.adminGetAllProviderHealth();
const providers = data.data || [];
if (providers.length === 0) {
el.innerHTML = 'No health data yet — health metrics are collected after the first completion request.
';
return;
}
// Stat cards
const total = providers.length;
const healthy = providers.filter(p => p.status === 'healthy').length;
const degraded = providers.filter(p => p.status === 'degraded').length;
const down = providers.filter(p => p.status === 'down' || p.status === 'unhealthy').length;
const avgLat = providers.reduce((s, p) => s + (p.avg_latency_ms || 0), 0);
const avgLatStr = total > 0 ? Math.round(avgLat / total) + 'ms' : '—';
if (cardsEl) {
cardsEl.innerHTML = `
Providers
${total}
All configured
Healthy
${healthy}
${total ? Math.round(healthy / total * 100) : 0}%
Degraded
${degraded + down}
${total ? Math.round((degraded + down) / total * 100) : 0}%
Avg Latency
${avgLatStr}
Last hour
`;
}
// Table
const rows = providers.map(p => {
const name = esc(p.provider_name || p.provider_config_id?.slice(0, 8) || '—');
const statusCls = p.status === 'healthy' ? 'badge-healthy' : p.status === 'degraded' ? 'badge-degraded' : p.status === 'down' || p.status === 'unhealthy' ? 'badge-unhealthy' : 'badge-unknown';
const latency = p.avg_latency_ms != null ? p.avg_latency_ms + 'ms' : '—';
const errorRate = p.error_rate != null ? (100 - p.error_rate * 100).toFixed(1) + '%' : '—';
const reqs = p.request_count ?? 0;
const errCount = (p.error_count || 0) + (p.timeout_count || 0);
const lastErr = p.last_error ? `${esc(p.last_error)}
` : '—';
return `
| ${name} |
${esc(p.status || 'unknown')} |
${latency} |
${errorRate} |
${reqs} |
${errCount} |
${lastErr} |
`;
}).join('');
el.innerHTML = `
| Provider | Status | Latency | Uptime | Requests | Errors | Last Error |
${rows}
`;
} catch (e) {
el.innerHTML = `Failed to load health data: ${esc(e.message)}
`;
}
},
// ── Routing Policies (v0.22.3) ───────────────
async loadAdminRouting() {
const listEl = document.getElementById('adminRoutingPolicyList');
const formEl = document.getElementById('adminRoutingForm');
listEl.innerHTML = 'Loading policies...
';
// Wire add button
const addBtn = document.getElementById('adminAddRoutingPolicyBtn');
addBtn?.addEventListener('click', () => UI._showRoutingForm(null), { once: true });
// Wire test button
const testBtn = document.getElementById('routingTestBtn');
testBtn?.addEventListener('click', () => UI._runRoutingTest(), { once: true });
try {
const data = await API.adminListRoutingPolicies();
const policies = data.data || [];
if (policies.length === 0) {
listEl.innerHTML = 'No routing policies — requests go to the user\'s default provider config.
';
return;
}
const typeLabels = { provider_prefer: 'Provider Prefer', team_route: 'Team Route', cost_limit: 'Cost Limit', model_alias: 'Model Alias' };
listEl.innerHTML = `
| Policy | Type | Scope | Priority | Status | |
${policies.map(p => `
| ${esc(p.name)} |
${esc(typeLabels[p.policy_type] || p.policy_type)} |
${p.scope}${p.team_id ? ' · ' + p.team_id.slice(0, 8) : ''} |
${p.priority} |
${p.is_active ? 'active' : 'inactive'} |
|
`).join('')}
`;
} catch (e) {
listEl.innerHTML = `Failed to load routing policies: ${esc(e.message)}
`;
}
},
async _showRoutingForm(policyId) {
const formEl = document.getElementById('adminRoutingForm');
const isEdit = !!policyId;
let policy = { name: '', scope: 'global', priority: 100, policy_type: 'provider_prefer', is_active: true, config: {} };
if (isEdit) {
try { policy = await API.adminGetRoutingPolicy(policyId); } catch (e) { UI.toast(e.message, 'error'); return; }
}
const cfg = policy.config || {};
formEl.style.display = '';
formEl.innerHTML = `
${isEdit ? 'Edit' : 'New'} Routing Policy
`;
document.getElementById('rpScope').addEventListener('change', (e) => {
document.getElementById('rpTeamGroup').style.display = e.target.value === 'team' ? '' : 'none';
});
document.getElementById('rpCancelBtn').addEventListener('click', () => { formEl.style.display = 'none'; });
document.getElementById('rpSaveBtn').addEventListener('click', async () => {
let config;
try { config = JSON.parse(document.getElementById('rpConfig').value || '{}'); }
catch (e) { UI.toast('Invalid JSON in config field', 'error'); return; }
const payload = {
name: document.getElementById('rpName').value,
priority: parseInt(document.getElementById('rpPriority').value) || 100,
policy_type: document.getElementById('rpType').value,
scope: document.getElementById('rpScope').value,
team_id: document.getElementById('rpScope').value === 'team' ? document.getElementById('rpTeamId').value : null,
is_active: document.getElementById('rpActive').checked,
config,
};
try {
if (isEdit) { await API.adminUpdateRoutingPolicy(policyId, payload); }
else { await API.adminCreateRoutingPolicy(payload); }
UI.toast(isEdit ? 'Policy updated' : 'Policy created', 'success');
formEl.style.display = 'none';
UI.loadAdminRouting();
} catch (e) { UI.toast(e.message, 'error'); }
});
},
async _toggleRoutingPolicy(id, active) {
try {
const policy = await API.adminGetRoutingPolicy(id);
policy.is_active = active;
await API.adminUpdateRoutingPolicy(id, policy);
UI.toast(active ? 'Policy enabled' : 'Policy disabled', 'success');
UI.loadAdminRouting();
} catch (e) { UI.toast(e.message, 'error'); }
},
async _deleteRoutingPolicy(id, name) {
if (!await showConfirm(`Delete routing policy "${name}"?`)) return;
try {
await API.adminDeleteRoutingPolicy(id);
UI.toast('Policy deleted', 'success');
UI.loadAdminRouting();
} catch (e) { UI.toast(e.message, 'error'); }
},
async _runRoutingTest() {
const model = document.getElementById('routingTestModel').value;
const userId = document.getElementById('routingTestUser').value || API.user?.id;
const resultEl = document.getElementById('routingTestResult');
if (!model) { UI.toast('Model is required', 'warning'); return; }
resultEl.style.display = '';
resultEl.innerHTML = 'Evaluating...
';
try {
const data = await API.adminTestRouting(model, userId);
const dec = data.decision;
const candidates = data.candidates || [];
resultEl.innerHTML = `
Decision: ${dec ? `${esc(dec.policy_name || 'no policy')} → ${esc(dec.selected?.slice(0, 8) || '—')}` : 'No routing applied'}
(${candidates.length} candidates, ${data.policies || 0} policies evaluated)
${candidates.map((c, i) => `
${i + 1}. ${esc(c.provider_id)} / ${esc(c.config_id?.slice(0, 8) || '—')}
${esc(c.status || 'unknown')}
${c.reason ? `${esc(c.reason)}` : ''}
`).join('')}
`;
} catch (e) {
resultEl.innerHTML = `Test failed: ${esc(e.message)}
`;
}
},
// ── Capability Overrides (v0.22.3) ───────────
async loadAdminCapabilities() {
const el = document.getElementById('adminCapabilityList');
el.innerHTML = 'Loading overrides...
';
try {
const data = await API.adminListCapabilityOverrides();
const overrides = data.data || [];
if (overrides.length === 0) {
el.innerHTML = 'No capability overrides — all models use auto-detected capabilities from catalog sync or heuristic inference.
';
return;
}
el.innerHTML = `
| Model | Provider Config | Field | Value | |
${overrides.map(o => `
${esc(o.model_id)} |
${esc(o.provider_config_id?.slice(0, 8) || 'any')} |
${esc(o.field)} |
${esc(String(o.value))} |
|
`).join('')}
`;
} catch (e) {
el.innerHTML = `Failed to load overrides: ${esc(e.message)}
`;
}
},
async _deleteCapOverride(modelId, overrideId) {
try {
await API.adminDeleteModelCapability(modelId, overrideId);
UI.toast('Override deleted', 'success');
UI.loadAdminCapabilities();
} catch (e) { UI.toast(e.message, 'error'); }
},
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 personas (policy: allow_user_personas)
document.getElementById('adminUserPersonasToggle').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 = '';
let modelList = typeof App !== 'undefined' && App.models ? App.models : [];
// On admin surface, App isn't loaded — fetch from admin API
if (modelList.length === 0 && typeof API !== 'undefined' && API.adminListModels) {
try {
const mr = await API.adminListModels();
modelList = (mr.models || mr.data || []).map(m => ({
id: m.model_id || m.id,
baseModelId: m.model_id || m.id,
name: m.display_name || m.model_id || m.id,
provider: m.provider_name || '',
hidden: !!m.hidden,
}));
} catch (e) { /* non-critical */ }
}
modelList.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('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();
// 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); }
},
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;
},
// ── Admin Channels (v0.23.2) ──────────────
async loadAdminChannels() {
const list = document.getElementById('adminArchivedList');
const countEl = document.getElementById('adminArchivedCount');
const modeEl = document.getElementById('adminRetentionMode');
if (!list) return;
try {
const resp = await API._get('/api/v1/admin/channels/archived');
const channels = resp.channels || [];
if (countEl) countEl.textContent = channels.length;
if (modeEl) modeEl.textContent = resp.retention_mode || 'flexible';
if (channels.length === 0) {
list.innerHTML = 'No archived channels
';
return;
}
let html = '' +
'| Title | Type | Owner | Messages | Archived | | ' +
'
';
for (const ch of channels) {
const age = _relativeTime(ch.updated_at);
html += `
| ${esc(ch.title || 'Untitled')} |
${esc(ch.type)} |
${esc(ch.owner_name)} |
${ch.message_count} |
${age} |
|
`;
}
html += '
';
list.innerHTML = html;
} catch (e) {
list.innerHTML = `Failed to load: ${esc(e.message)}
`;
}
},
async _purgeChannel(channelId) {
if (!await showConfirm('Permanently delete this archived channel and all its messages? This cannot be undone.')) return;
try {
await API._del(`/api/v1/admin/channels/${channelId}/purge`);
UI.toast('Channel purged', 'success');
UI.loadAdminChannels(); // refresh list
} catch (e) {
UI.toast('Purge failed: ' + e.message, 'error');
}
},
});