/** * Audit Analytics Dashboard * Displays governance decision analytics from MemoryProxy audit trail */ let auditData = []; // Get auth token from localStorage function getAuthToken() { return localStorage.getItem('admin_token'); } // Check authentication function checkAuth() { const token = getAuthToken(); if (!token) { window.location.href = '/admin/login.html'; return false; } return true; } // Load audit data from API async function loadAuditData() { console.log('[Audit Analytics] Loading audit data...'); try { const token = getAuthToken(); console.log('[Audit Analytics] Token:', token ? 'Present' : 'Missing'); const response = await fetch('/api/admin/audit-logs?days=30', { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); console.log('[Audit Analytics] Response status:', response.status); if (response.status === 401) { console.log('[Audit Analytics] Unauthorized - redirecting to login'); localStorage.removeItem('admin_token'); localStorage.removeItem('admin_user'); window.location.href = '/admin/login.html'; return; } const data = await response.json(); console.log('[Audit Analytics] Data received:', data); if (data.success) { auditData = data.decisions || []; console.log('[Audit Analytics] Audit data loaded:', auditData.length, 'decisions'); renderDashboard(); } else { console.error('[Audit Analytics] API returned error:', data.error); showError('Failed to load audit data: ' + (data.error || 'Unknown error')); } } catch (error) { console.error('[Audit Analytics] Error loading audit data:', error); showError('Error loading audit data. Please check console for details.'); } } // Render dashboard function renderDashboard() { updateSummaryCards(); renderActionChart(); renderServiceChart(); renderServiceHealth24h(); renderViolations7days(); renderTimelineChart(); renderAuditTable(); } // Update summary cards function updateSummaryCards() { const totalDecisions = auditData.length; const allowedCount = auditData.filter(d => d.allowed).length; const violationsCount = auditData.filter(d => d.violations && d.violations.length > 0).length; const servicesSet = new Set(auditData.map(d => d.service).filter(s => s && s !== 'unknown')); document.getElementById('total-decisions').textContent = totalDecisions; document.getElementById('allowed-rate').textContent = totalDecisions > 0 ? `${((allowedCount / totalDecisions) * 100).toFixed(1)}%` : '0%'; document.getElementById('violations-count').textContent = violationsCount; document.getElementById('services-count').textContent = servicesSet.size || 0; } // Render action type chart function renderActionChart() { const actionCounts = {}; auditData.forEach(decision => { const action = decision.action || 'unknown'; actionCounts[action] = (actionCounts[action] || 0) + 1; }); const chartEl = document.getElementById('action-chart'); if (Object.keys(actionCounts).length === 0) { chartEl.innerHTML = '

No data available

'; return; } const sorted = Object.entries(actionCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 10); const maxCount = Math.max(...sorted.map(([, count]) => count)); const html = sorted.map(([action, count]) => { const percentage = (count / maxCount) * 100; const label = action.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); return `
${label} ${count}
`; }).join(''); chartEl.innerHTML = html; setProgressBarWidths(chartEl); } // Render service chart function renderServiceChart() { const serviceCounts = {}; auditData.forEach(decision => { const service = decision.service || 'unknown'; serviceCounts[service] = (serviceCounts[service] || 0) + 1; }); const chartEl = document.getElementById('service-chart'); if (Object.keys(serviceCounts).length === 0) { chartEl.innerHTML = '

No data available

'; return; } const sorted = Object.entries(serviceCounts) .sort((a, b) => b[1] - a[1]); const maxCount = Math.max(...sorted.map(([, count]) => count)); // Color palette for services const colors = [ 'bg-blue-600', 'bg-green-600', 'bg-purple-600', 'bg-orange-600', 'bg-pink-600', 'bg-indigo-600', 'bg-red-600', 'bg-yellow-600' ]; const html = sorted.map(([service, count], index) => { const percentage = (count / maxCount) * 100; // Ensure minimum 8% width so all bars are visible const displayPercentage = Math.max(percentage, 8); const color = colors[index % colors.length]; const label = service === 'unknown' ? 'Unknown' : service; return `
${label} ${count}
`; }).join(''); chartEl.innerHTML = html; setProgressBarWidths(chartEl); } // Render service health (24h) function renderServiceHealth24h() { const chartEl = document.getElementById('service-health-24h'); // Filter last 24 hours const now = new Date(); const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000); const last24h = auditData.filter(d => new Date(d.timestamp) >= twentyFourHoursAgo); if (last24h.length === 0) { chartEl.innerHTML = '

No data in last 24 hours

'; return; } // Group by service const serviceStats = {}; last24h.forEach(decision => { const service = decision.service || 'unknown'; if (!serviceStats[service]) { serviceStats[service] = { allowed: 0, blocked: 0, violations: 0 }; } if (decision.allowed) { serviceStats[service].allowed++; } else { serviceStats[service].blocked++; } if (decision.violations && decision.violations.length > 0) { serviceStats[service].violations += decision.violations.length; } }); const html = Object.entries(serviceStats).map(([service, stats]) => { const isHealthy = stats.blocked === 0 && stats.violations === 0; const bgColor = isHealthy ? 'bg-green-50' : 'bg-red-50'; const borderColor = isHealthy ? 'border-green-200' : 'border-red-200'; const icon = isHealthy ? '✓' : '⚠'; const iconColor = isHealthy ? 'text-green-600' : 'text-red-600'; return `
${service === 'unknown' ? 'Unknown' : service} ${icon}

✓ Allowed: ${stats.allowed}

${stats.blocked > 0 ? `

✗ Blocked: ${stats.blocked}

` : ''} ${stats.violations > 0 ? `

⚠ Violations: ${stats.violations}

` : ''}
`; }).join(''); chartEl.innerHTML = html; } // Render violations (7 days) function renderViolations7days() { const chartEl = document.getElementById('violations-7days'); // Filter last 7 days const now = new Date(); const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); const last7days = auditData.filter(d => new Date(d.timestamp) >= sevenDaysAgo); // Group by day const dayStats = {}; last7days.forEach(decision => { const date = new Date(decision.timestamp); const dayKey = date.toISOString().split('T')[0]; // YYYY-MM-DD if (!dayStats[dayKey]) { dayStats[dayKey] = { date: date, total: 0, blocked: 0, violations: 0, services: new Set() }; } dayStats[dayKey].total++; if (!decision.allowed) { dayStats[dayKey].blocked++; } if (decision.violations && decision.violations.length > 0) { dayStats[dayKey].violations += decision.violations.length; } if (decision.service && decision.service !== 'unknown') { dayStats[dayKey].services.add(decision.service); } }); // Check if there are any violations or blocks const hasIssues = Object.values(dayStats).some(stats => stats.blocked > 0 || stats.violations > 0); if (!hasIssues) { chartEl.innerHTML = `

✓ No violations or blocks in the last 7 days

All governance decisions passed successfully

`; return; } // Show only days with issues const daysWithIssues = Object.entries(dayStats) .filter(([, stats]) => stats.blocked > 0 || stats.violations > 0) .sort((a, b) => b[1].date - a[1].date); // Most recent first const html = daysWithIssues.map(([dayKey, stats]) => { const dayLabel = stats.date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }); const servicesInvolved = Array.from(stats.services).join(', '); return `

${dayLabel}

Total: ${stats.total} decisions

${stats.blocked > 0 ? `

✗ Blocked: ${stats.blocked}

` : ''} ${stats.violations > 0 ? `

⚠ Violations: ${stats.violations}

` : ''} ${servicesInvolved ? `

Services: ${servicesInvolved}

` : ''}
`; }).join(''); chartEl.innerHTML = html; } // Timeline mode state let timelineMode = 'daily'; // Switch timeline mode function switchTimelineMode(mode) { timelineMode = mode; // Update button styles document.querySelectorAll('[data-timeline-mode]').forEach(btn => { if (btn.dataset.timelineMode === mode) { btn.className = 'px-3 py-1 text-sm rounded bg-purple-600 text-white hover:bg-purple-700 transition'; } else { btn.className = 'px-3 py-1 text-sm rounded bg-gray-200 text-gray-700 hover:bg-gray-300 transition'; } }); renderTimelineChart(); } // Render timeline chart function renderTimelineChart() { const chartEl = document.getElementById('timeline-chart'); if (auditData.length === 0) { chartEl.innerHTML = '

No data available

'; return; } const now = new Date(); let buckets = []; let filteredData = []; if (timelineMode === '6hourly') { // Last 24 hours in 6-hour buckets const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000); filteredData = auditData.filter(d => new Date(d.timestamp) >= twentyFourHoursAgo); // Create 4 buckets: 0-6h, 6-12h, 12-18h, 18-24h ago for (let i = 3; i >= 0; i--) { const bucketEnd = new Date(now.getTime() - i * 6 * 60 * 60 * 1000); const bucketStart = new Date(bucketEnd.getTime() - 6 * 60 * 60 * 1000); buckets.push({ label: i === 0 ? 'Last 6h' : `${i * 6}-${(i + 1) * 6}h ago`, start: bucketStart, end: bucketEnd, count: 0 }); } } else if (timelineMode === 'daily') { // Last 7 days const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); filteredData = auditData.filter(d => new Date(d.timestamp) >= sevenDaysAgo); // Create 7 daily buckets for (let i = 6; i >= 0; i--) { const bucketEnd = new Date(now); bucketEnd.setHours(23, 59, 59, 999); bucketEnd.setDate(bucketEnd.getDate() - i); const bucketStart = new Date(bucketEnd); bucketStart.setHours(0, 0, 0, 0); buckets.push({ label: bucketStart.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }), start: bucketStart, end: bucketEnd, count: 0 }); } } else if (timelineMode === 'weekly') { // Last 4 weeks const fourWeeksAgo = new Date(now.getTime() - 28 * 24 * 60 * 60 * 1000); filteredData = auditData.filter(d => new Date(d.timestamp) >= fourWeeksAgo); // Create 4 weekly buckets for (let i = 3; i >= 0; i--) { const bucketEnd = new Date(now.getTime() - i * 7 * 24 * 60 * 60 * 1000); const bucketStart = new Date(bucketEnd.getTime() - 7 * 24 * 60 * 60 * 1000); buckets.push({ label: `Week ${4 - i}`, start: bucketStart, end: bucketEnd, count: 0 }); } } // Count decisions in each bucket filteredData.forEach(decision => { const timestamp = new Date(decision.timestamp); for (const bucket of buckets) { if (timestamp >= bucket.start && timestamp <= bucket.end) { bucket.count++; break; } } }); const maxCount = Math.max(...buckets.map(b => b.count), 1); const containerHeight = 192; // h-48 = 192px const html = buckets.map(bucket => { const percentage = (bucket.count / maxCount) * 100; const minPercentage = Math.max(percentage, 5); const pixelHeight = Math.round((minPercentage / 100) * containerHeight); return `
${bucket.count}
 
${bucket.label}
`; }).join(''); chartEl.innerHTML = `
${html}
`; setProgressBarWidths(chartEl); } // Render audit table function renderAuditTable() { const tbody = document.getElementById('audit-log-tbody'); if (auditData.length === 0) { tbody.innerHTML = 'No audit data available'; return; } const recent = auditData.slice(0, 10); const html = recent.map(decision => { const timestamp = new Date(decision.timestamp).toLocaleString(); const action = decision.action || 'Unknown'; const sessionId = decision.sessionId || 'N/A'; const allowed = decision.allowed; const violations = decision.violations || []; const statusClass = allowed ? 'text-green-600 bg-green-100' : 'text-red-600 bg-red-100'; const statusText = allowed ? 'Allowed' : 'Blocked'; const violationsText = violations.length > 0 ? violations.join(', ') : 'None'; return ` ${timestamp} ${action} ${sessionId.substring(0, 20)}... ${statusText} ${violationsText.substring(0, 40)}${violationsText.length > 40 ? '...' : ''} `; }).join(''); tbody.innerHTML = html; } // Show decision details function showDecisionDetails(timestamp) { const decision = auditData.find(d => d.timestamp === timestamp); if (!decision) return; alert(`Decision Details:\n\n${JSON.stringify(decision, null, 2)}`); } // Show error function showError(message) { const tbody = document.getElementById('audit-log-tbody'); tbody.innerHTML = `${message}`; } // Initialize function init() { if (!checkAuth()) return; // Setup refresh button const refreshBtn = document.getElementById('refresh-btn'); if (refreshBtn) { console.log('[Audit Analytics] Refresh button found, attaching event listener'); refreshBtn.addEventListener('click', () => { console.log('[Audit Analytics] Refresh button clicked, loading data...'); loadAuditData(); }); } else { console.error('[Audit Analytics] Refresh button not found!'); } // Load initial data loadAuditData(); } // Run initialization init(); // Set widths/heights from data attributes (CSP compliance) function setProgressBarWidths(container) { const elements = container.querySelectorAll('[data-width], [data-height], [data-pixel-height]'); elements.forEach(el => { if (el.dataset.width) { el.style.width = el.dataset.width + '%'; } if (el.dataset.height) { el.style.height = el.dataset.height + '%'; } if (el.dataset.pixelHeight) { const height = Math.max(parseInt(el.dataset.pixelHeight), 10); // Minimum 10px el.style.height = height + 'px'; } }); } // Event delegation for data-action buttons (CSP compliance) document.addEventListener('click', (e) => { const button = e.target.closest('[data-action]'); if (!button) return; const action = button.dataset.action; const arg0 = button.dataset.arg0; if (action === 'showDecisionDetails') { showDecisionDetails(arg0); } else if (action === 'switchTimelineMode') { switchTimelineMode(arg0); } });