/** * agents-controller.js - Agent Profile Dashboard Controller * ========================================================== * Handles agent list, detail views, charts, search, and sort. * * Hardened for Enterprise MVC: * - Uses central `apiFetch` to automatically inherit HTTP-Only Cookies and CSRF headers. * - Uses `SecurityUtils` for XSS-safe DOM generation. * - Uses `ErrorHandler` for standardized exception recovery. */ (function() { 'use strict'; let allAgents = []; let currentAgentId = null; let f1TrendChart = null; let qualityRadarChart = null; // DOM Elements const agentsListView = document.getElementById('agents-list-view'); const agentDetailView = document.getElementById('agent-detail-view'); const agentsGrid = document.getElementById('agents-grid'); const agentsEmptyState = document.getElementById('agents-empty-state'); const agentSearch = document.getElementById('agent-search'); const sortSelect = document.getElementById('sort-select'); const backToListBtn = document.getElementById('back-to-list-btn'); const reindexAgentBtn = document.getElementById('reindex-agent-btn'); // Stats elements const totalAgentsEl = document.getElementById('total-agents'); const avgF1ScoreEl = document.getElementById('avg-f1-score'); const improvingCountEl = document.getElementById('improving-count'); const decliningCountEl = document.getElementById('declining-count'); // Detail elements const detailAgentName = document.getElementById('detail-agent-name'); const detailAgentEmail = document.getElementById('detail-agent-email'); const detailTotalAudits = document.getElementById('detail-total-audits'); const detailAvgF1 = document.getElementById('detail-avg-f1'); const detailTrend = document.getElementById('detail-trend'); const detailRiskFlags = document.getElementById('detail-risk-flags'); // Safe HTML Escaping (Fallback to native if SecurityUtils isn't loaded yet) const escapeHTML = (text) => { if (window.SecurityUtils) return window.SecurityUtils.escapeHTML(text); const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }; async function init() { await loadAgents(); setupEventListeners(); } async function loadAgents() { if (window.toggleLoader) window.toggleLoader(true, 'Synchronizing agent profiles...'); try { // Enterprise Architecture: Centralized apiFetch handles CSRF and Cookies automatically const data = await window.apiFetch('/agents'); if (data.success) { allAgents = data.agents || []; renderAgentsList(allAgents); updateStatsBar(allAgents); } } catch (error) { console.error('[Agents] Load failed:', error); if (window.ErrorHandler) { window.ErrorHandler.showError(error); } else if (window.SecurityUtils) { window.SecurityUtils.showToast('Failed to load agents', 'error'); } if (agentsGrid) agentsGrid.innerHTML = ''; if (agentsEmptyState) agentsEmptyState.hidden = false; updateStatsBar([]); } finally { if (window.toggleLoader) window.toggleLoader(false); } } function renderAgentsList(agents) { if (!agentsGrid || !agentsEmptyState) return; if (!agents || agents.length === 0) { agentsGrid.innerHTML = ''; agentsEmptyState.hidden = false; return; } agentsEmptyState.hidden = true; agentsGrid.innerHTML = agents.map(agent => { const stats = agent.stats || {}; const trendIcon = getTrendIcon(stats.trend_direction); const trendClass = getTrendClass(stats.trend_direction); const f1Score = (stats.avg_f1_score || 0).toFixed(2); return `
person

${escapeHTML(agent.name || 'Unknown')}

${escapeHTML(agent.email || '')}

${trendIcon}
F1 Score${f1Score}
Audits${escapeHTML(stats.total_audits || 0)}
Risk${escapeHTML((stats.red_risk_count || 0) + (stats.amber_risk_count || 0))}
`; }).join(''); document.querySelectorAll('.agent-card').forEach(card => { card.addEventListener('click', () => loadAgentDetail(card.dataset.agentId)); card.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); loadAgentDetail(card.dataset.agentId); } }); }); } function updateStatsBar(agents) { const total = agents.length; const avg = total > 0 ? (agents.reduce((s, a) => s + (a.stats?.avg_f1_score || 0), 0) / total).toFixed(2) : '0.00'; if (totalAgentsEl) totalAgentsEl.textContent = total; if (avgF1ScoreEl) avgF1ScoreEl.textContent = avg; if (improvingCountEl) improvingCountEl.textContent = agents.filter(a => a.stats?.trend_direction === 'improving').length; if (decliningCountEl) decliningCountEl.textContent = agents.filter(a => a.stats?.trend_direction === 'declining').length; } async function loadAgentDetail(agentId) { currentAgentId = agentId; if (window.toggleLoader) window.toggleLoader(true, 'Loading profile...'); try { const data = await window.apiFetch(`/agents/${agentId}`); if (data.success) { renderAgentDetail(data.agent); showDetailView(); } } catch (error) { console.error('[Agents] Profile load failed:', error); if (window.ErrorHandler) { window.ErrorHandler.showError(error); } else if (window.SecurityUtils) { window.SecurityUtils.showToast('Failed to load agent profile', 'error'); } } finally { if (window.toggleLoader) window.toggleLoader(false); } } function renderAgentDetail(agent) { const s = agent.stats || {}; if (detailAgentName) { detailAgentName.textContent = agent.name || 'Unknown'; // Accessibility / UX: expose the full name on hover via title and aria-label detailAgentName.title = agent.name || ''; detailAgentName.setAttribute('aria-label', agent.name || 'Agent Name'); } if (detailAgentEmail) { detailAgentEmail.textContent = agent.email || ''; detailAgentEmail.title = agent.email || ''; } if (detailTotalAudits) detailTotalAudits.textContent = s.total_audits || 0; if (detailAvgF1) detailAvgF1.textContent = (s.avg_f1_score || 0).toFixed(2); if (detailTrend) { detailTrend.textContent = (s.trend_direction || 'stable').toUpperCase(); detailTrend.className = `detail-stat-value trend-${s.trend_direction || 'stable'}`; } if (detailRiskFlags) detailRiskFlags.textContent = (s.red_risk_count || 0) + (s.amber_risk_count || 0); if (window.Chart) { renderF1TrendChart(agent.score_history || []); renderQualityRadarChart(s); } renderScoreHistoryTable(agent.score_history || []); if (reindexAgentBtn) reindexAgentBtn.dataset.agentId = agent.agent_id; } function renderF1TrendChart(history) { const ctx = document.getElementById('f1-trend-chart')?.getContext('2d'); if (!ctx) return; if (f1TrendChart) f1TrendChart.destroy(); // Ensure history is chronological for standard L-to-R line chart const data = history.slice().reverse().map(h => h.f1_score || 0); const labels = data.map((_, i) => `A${i+1}`); // CSS Variable support for themes const rootStyles = getComputedStyle(document.documentElement); const primaryColor = rootStyles.getPropertyValue('--md-primary').trim() || '#14B8A6'; const primaryLight = 'rgba(20, 184, 166, 0.1)'; f1TrendChart = new Chart(ctx, { type: 'line', data: { labels, datasets: [{ label: 'F1 Score', data, borderColor: primaryColor, backgroundColor: primaryLight, fill: true, tension: 0.4 }] }, options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { min: 0, max: 1 }, x: { ticks: { maxTicksLimit: 10 } } } } }); } function renderQualityRadarChart(stats) { const ctx = document.getElementById('quality-radar-chart')?.getContext('2d'); if (!ctx) return; if (qualityRadarChart) qualityRadarChart.destroy(); const rootStyles = getComputedStyle(document.documentElement); const accentColor = rootStyles.getPropertyValue('--md-secondary').trim() || '#a78bfa'; const accentLight = 'rgba(167, 139, 250, 0.2)'; qualityRadarChart = new Chart(ctx, { type: 'radar', data: { labels: ['Empathy', 'Efficiency', 'Listening', 'Language', 'Bias'], datasets: [{ data: [stats.avg_empathy||0, stats.avg_efficiency||0, stats.avg_listening||0, stats.avg_language||0, stats.avg_bias||0], borderColor: accentColor, backgroundColor: accentLight, pointBackgroundColor: accentColor }] }, options: { responsive: true, plugins: { legend: { display: false } }, scales: { r: { beginAtZero: true, max: 10, ticks: { display: false } } } } }); } function renderScoreHistoryTable(history) { const table = document.getElementById('score-history-table'); if (!table) return; if (!history.length) { table.innerHTML = '

No history found

'; return; } table.innerHTML = ` ${history.slice(0, 10).map(h => ` `).join('')}
DateF1RiskActions
${escapeHTML(formatDate(h.audited_at))} ${(h.f1_score || 0).toFixed(2)} ${escapeHTML(h.compliance_risk || 'Green')} View
`; // Ensure navigation works: if we're already on the Audit workspace, // load the audit in-place; otherwise perform a full navigation to // `/audit?id=...` so the audit page renders the dashboard exactly // as it appears in the archive view. const links = table.querySelectorAll('.view-audit-link'); links.forEach(link => { link.addEventListener('click', (e) => { // Allow user to open in new tab/window if (e.ctrlKey || e.metaKey || e.button === 1) return; e.preventDefault(); try { const href = link.getAttribute('href') || ''; const qs = href.split('?')[1] || ''; const params = new URLSearchParams(qs); const auditId = params.get('id'); // If we're already on the Audit page, prefer the in-page loader. const onAuditPage = !!document.getElementById('page-audit-main'); if (onAuditPage && auditId && typeof window.loadAuditDetails === 'function') { window.loadAuditDetails(auditId); return; } // Otherwise navigate to the audit page so it can render the // full audit dashboard (archive-like experience). if (href) window.location.href = href; } catch (err) { const href = link.getAttribute('href'); if (href) window.location.href = href; } }); }); } function searchAgents(term) { const t = term.toLowerCase().trim(); renderAgentsList(t ? allAgents.filter(a => (a.name||'').toLowerCase().includes(t) || (a.email||'').toLowerCase().includes(t)) : allAgents); } function sortAgents(sortBy) { const sorted = [...allAgents].sort((a, b) => { if (sortBy === 'f1_desc') return (b.stats?.avg_f1_score || 0) - (a.stats?.avg_f1_score || 0); if (sortBy === 'f1_asc') return (a.stats?.avg_f1_score || 0) - (b.stats?.avg_f1_score || 0); if (sortBy === 'name_asc') return (a.name || '').localeCompare(b.name || ''); if (sortBy === 'audits_desc') return (b.stats?.total_audits || 0) - (a.stats?.total_audits || 0); return 0; }); renderAgentsList(sorted); } async function reindexAgent() { if (!currentAgentId) return; if (window.SecurityUtils && reindexAgentBtn) { window.SecurityUtils.setButtonLoading(reindexAgentBtn, true); } try { const data = await window.apiFetch(`/agents/${currentAgentId}/reindex`, { method: 'POST' }); if (data.success) { if (window.SecurityUtils) window.SecurityUtils.showToast('Profile reindex queued.', 'success'); setTimeout(() => loadAgentDetail(currentAgentId), 2000); } } catch (error) { if (window.ErrorHandler) { window.ErrorHandler.showError(error); } else if (window.SecurityUtils) { window.SecurityUtils.showToast('Reindex failed', 'error'); } } finally { if (window.SecurityUtils && reindexAgentBtn) { window.SecurityUtils.setButtonLoading(reindexAgentBtn, false); } } } function showDetailView() { if (agentsListView) agentsListView.classList.remove('active'); if (agentDetailView) agentDetailView.classList.add('active'); window.scrollTo(0,0); } function showListView() { if (agentDetailView) agentDetailView.classList.remove('active'); if (agentsListView) agentsListView.classList.add('active'); currentAgentId = null; } function setupEventListeners() { if (agentSearch) agentSearch.addEventListener('input', e => searchAgents(e.target.value)); if (sortSelect) sortSelect.addEventListener('change', e => sortAgents(e.target.value)); if (backToListBtn) backToListBtn.addEventListener('click', showListView); if (reindexAgentBtn) reindexAgentBtn.addEventListener('click', reindexAgent); // Auto-load when agents tab is clicked const agentsTab = document.querySelector('[data-tab="agents"]'); if (agentsTab) { agentsTab.addEventListener('click', () => { // Short timeout to allow panel transition setTimeout(() => { if (allAgents.length === 0) loadAgents(); }, 50); }); } } // Helpers function getTrendIcon(t) { return t === 'improving' ? 'trending_up' : t === 'declining' ? 'trending_down' : 'trending_flat'; } function getTrendClass(t) { return `trend-${t || 'stable'}`; } function formatDate(d) { return d ? new Date(d).toLocaleDateString() : 'Never'; } // Init if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } // Expose reload capability globally window.AgentsController = { reload: () => loadAgents(), showList: showListView }; })();