/** * Audit Analytics Dashboard * Displays governance decision analytics from MemoryProxy audit trail */ let auditData = []; // Rule descriptions for tooltips and legend const RULE_DESCRIPTIONS = { 'inst_072': { title: 'Defense-in-Depth Credential Protection', description: 'Multi-layer security protection for credentials and sensitive data. Prevents credential leaks through 5 layers: prevention, mitigation, detection, backstop, and recovery.' }, 'inst_084': { title: 'GitHub Repository URL Protection', description: 'Hard block on GitHub URL modifications. Prevents accidental exposure of private repository structure or incorrect links.' }, 'inst_038': { title: 'Pre-Action Validation Required', description: 'Mandatory pre-action checks before file modifications. Validates context pressure, instruction history, token checkpoints, and CSP compliance.' }, 'inst_008': { title: 'Content Security Policy (CSP) Compliance', description: 'Enforces strict CSP rules to prevent XSS attacks. No inline scripts or styles allowed in HTML/JS files.' }, 'inst_016': { title: 'Prohibited Terms: "Guardrails"', description: 'Bans "guardrails" terminology as conceptually inadequate for AI safety. Use "governance framework" instead.' }, 'inst_017': { title: 'Prohibited Terms: "Alignment"', description: 'Bans single "alignment" in favor of explicit pluralistic deliberation. Prevents false consensus assumptions.' }, 'inst_018': { title: 'Prohibited Terms: "Safety Measures"', description: 'Bans vague "safety measures" language. Requires specific, verifiable safety mechanisms.' } }; // 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'); // Build query parameters const environment = document.getElementById('environment-filter')?.value || 'all'; let url = '/api/admin/audit-logs?days=30'; if (environment !== 'all') { url += `&environment=${environment}`; } console.log('[Audit Analytics] Fetching from:', url); const response = await fetch(url, { 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 async function renderDashboard() { updateSummaryCards(); await renderBusinessIntelligence(); renderFrameworkSaves(); renderBlockReasons(); renderSeverityBreakdown(); renderActionChart(); renderServiceChart(); renderServiceHealth24h(); renderViolations7days(); renderTimelineChart(); renderAuditTable(); } // Update summary cards function updateSummaryCards() { const totalDecisions = auditData.length; const allowedCount = auditData.filter(d => d.allowed).length; const blockedCount = auditData.filter(d => !d.allowed).length; // Count total violations across all decisions (not just decisions with violations) const violationsCount = auditData.reduce((sum, d) => sum + (d.violations ? d.violations.length : 0), 0); const servicesSet = new Set(auditData.map(d => d.service).filter(s => s && s !== 'unknown')); document.getElementById('total-decisions').textContent = totalDecisions; document.getElementById('allowed-count').textContent = allowedCount; document.getElementById('allowed-rate').textContent = totalDecisions > 0 ? `${((allowedCount / totalDecisions) * 100).toFixed(1)}%` : '0%'; document.getElementById('blocked-count').textContent = blockedCount; document.getElementById('block-rate').textContent = totalDecisions > 0 ? `${((blockedCount / totalDecisions) * 100).toFixed(1)}%` : '0%'; document.getElementById('violations-count').textContent = violationsCount; document.getElementById('services-count').textContent = servicesSet.size || 0; // Environment distribution breakdown updateEnvironmentDistribution(); // Data quality insights updateDataInsights(blockedCount, violationsCount); } // Update environment distribution display function updateEnvironmentDistribution() { const environmentStats = { development: 0, production: 0, unspecified: 0 }; auditData.forEach(d => { if (d.environment === 'development') { environmentStats.development++; } else if (d.environment === 'production') { environmentStats.production++; } else { environmentStats.unspecified++; } }); const total = auditData.length; const statsEl = document.getElementById('environment-stats'); statsEl.innerHTML = `
Development
${environmentStats.development}
${total > 0 ? ((environmentStats.development / total) * 100).toFixed(1) : 0}%
Production
${environmentStats.production}
${total > 0 ? ((environmentStats.production / total) * 100).toFixed(1) : 0}%
Unspecified
${environmentStats.unspecified}
${total > 0 ? ((environmentStats.unspecified / total) * 100).toFixed(1) : 0}%
`; // Show warning if there are unspecified records if (environmentStats.unspecified > 0) { const warningHTML = `
โš ๏ธ Note: ${environmentStats.unspecified} record(s) have no environment field. These are included in "All Environments" but excluded when filtering by Development or Production.
`; statsEl.innerHTML += warningHTML; } } // Update data quality insights function updateDataInsights(blockedCount, violationsCount) { const insightsEl = document.getElementById('data-insights'); // Check if violations > blocked (indicates some decisions had multiple violations) if (violationsCount > blockedCount && blockedCount > 0) { const multipleViolationDecisions = auditData.filter(d => !d.allowed && d.violations && d.violations.length > 1 ).length; const avgViolationsPerBlock = (violationsCount / blockedCount).toFixed(1); insightsEl.innerHTML = `

๐Ÿ“Š Data Quality Insight: Multiple Violations Per Decision

${violationsCount} violations occurred across ${blockedCount} blocked decisions (${avgViolationsPerBlock} violations per block on average).

${multipleViolationDecisions > 0 ? `${multipleViolationDecisions} decision(s) triggered multiple rule violations simultaneously (e.g., a file with both inline styles AND inline event handlers).` : 'This indicates violations are being tracked granularly with detailed rule breakdowns.' }

โœ“ This is expected behavior - each specific violation is logged separately for audit trail precision
`; } else if (violationsCount === blockedCount && blockedCount > 0) { insightsEl.innerHTML = `

โœ“ Data Quality: 1:1 Block-to-Violation Ratio

Each blocked decision corresponds to exactly one rule violation. Clean, single-violation blocks indicate precise governance enforcement.

`; } else { // No insights to show insightsEl.innerHTML = ''; } } // Render Business Intelligence async function renderBusinessIntelligence() { // Activity Type Breakdown const byActivityType = {}; auditData.forEach(d => { const type = d.activityType || 'Unknown'; if (!byActivityType[type]) { byActivityType[type] = { total: 0, allowed: 0, blocked: 0 }; } byActivityType[type].total++; if (d.allowed) { byActivityType[type].allowed++; } else { byActivityType[type].blocked++; } }); const activityEl = document.getElementById('activity-type-breakdown'); activityEl.innerHTML = ''; const sortedActivity = Object.entries(byActivityType).sort((a, b) => b[1].blocked - a[1].blocked); if (sortedActivity.length === 0) { activityEl.innerHTML = '

No activity data available

'; } else { sortedActivity.forEach(([type, data]) => { const blockRate = (data.blocked / data.total * 100).toFixed(1); const isHighRisk = data.blocked > 0; const activityDiv = document.createElement('div'); activityDiv.className = `flex items-center justify-between p-3 rounded-lg border ${isHighRisk ? 'border-orange-200 bg-orange-50' : 'border-gray-200 bg-gray-50'}`; const leftDiv = document.createElement('div'); leftDiv.innerHTML = `
${type}
${data.total} total ยท ${data.blocked} blocked ยท ${blockRate}% block rate
`; const badge = document.createElement('span'); // Determine risk level based on block rate const riskLevel = data.blocked === 0 ? 'Clean' : parseFloat(blockRate) >= 10 ? 'High Risk' : parseFloat(blockRate) >= 5 ? 'Medium Risk' : 'Low Risk'; const badgeColor = data.blocked === 0 ? 'bg-green-100 text-green-800' : parseFloat(blockRate) >= 10 ? 'bg-red-100 text-red-800' : parseFloat(blockRate) >= 5 ? 'bg-orange-100 text-orange-800' : 'bg-yellow-100 text-yellow-800'; badge.className = `px-3 py-1 rounded-full text-xs font-medium ${badgeColor}`; badge.textContent = riskLevel; activityDiv.appendChild(leftDiv); activityDiv.appendChild(badge); activityEl.appendChild(activityDiv); }); } // Cost Avoidance - filter by selected period const period = document.getElementById('cost-period-selector')?.value || '30'; let filteredData = auditData; if (period !== 'all') { const days = parseInt(period); const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - days); filteredData = auditData.filter(d => new Date(d.timestamp) >= cutoffDate); } const blockedDecisions = filteredData.filter(d => !d.allowed); let totalCost = 0; const costByLevel = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 }; // Load cost factors from API (fallback to defaults) const apiCostFactors = await loadCostConfig(); const costFactors = apiCostFactors ? { CRITICAL: apiCostFactors.CRITICAL.amount, HIGH: apiCostFactors.HIGH.amount, MEDIUM: apiCostFactors.MEDIUM.amount, LOW: apiCostFactors.LOW.amount } : { CRITICAL: 50000, HIGH: 10000, MEDIUM: 2000, LOW: 500 }; blockedDecisions.forEach(d => { if (d.violations && d.violations.length > 0) { d.violations.forEach(v => { const severity = v.severity || 'LOW'; const cost = costFactors[severity] || 0; totalCost += cost; costByLevel[severity] += cost; }); } }); document.getElementById('cost-avoidance-total').textContent = `$${totalCost.toLocaleString()}`; const breakdownEl = document.getElementById('cost-avoidance-breakdown'); breakdownEl.innerHTML = Object.entries(costByLevel) .filter(([_, cost]) => cost > 0) .map(([level, cost]) => `
${level}: $${cost.toLocaleString()}
`) .join(''); // Framework Maturity Score const recentBlockRate = blockedDecisions.length / Math.max(auditData.length, 1); const maturityScore = Math.max(0, Math.min(100, Math.round(100 - (recentBlockRate * 500)))); document.getElementById('maturity-score').textContent = maturityScore; document.getElementById('maturity-trend').innerHTML = maturityScore > 80 ? 'โ†‘ Excellent' : maturityScore > 60 ? 'โ†’ Good' : 'โ†— Learning'; document.getElementById('maturity-message').textContent = maturityScore > 80 ? 'Framework teaching good practices' : maturityScore > 60 ? 'Team adapting well to governance' : 'Framework actively preventing violations'; const progressBar = document.getElementById('maturity-progress'); progressBar.style.width = maturityScore + '%'; // PHASE 3.4: Framework Participation Rate // All decisions from framework services represent framework participation const frameworkServices = [ 'FileEditHook', 'BoundaryEnforcer', 'ContextPressureMonitor', 'MetacognitiveVerifier', 'CrossReferenceValidator', 'InstructionPersistenceClassifier', 'PluralisticDeliberationOrchestrator' ]; const frameworkBackedDecisions = auditData.filter(d => frameworkServices.includes(d.service) ); const participationRate = auditData.length > 0 ? ((frameworkBackedDecisions.length / auditData.length) * 100).toFixed(1) : '0.0'; document.getElementById('participation-rate').textContent = `${participationRate}%`; // Message based on participation rate const rate = parseFloat(participationRate); let participationMessage = ''; if (rate >= 80) { participationMessage = 'Excellent - Framework actively guiding most decisions'; } else if (rate >= 60) { participationMessage = 'Good - Framework participating in majority of decisions'; } else if (rate >= 40) { participationMessage = 'Moderate - Framework guidance available for some decisions'; } else if (rate >= 20) { participationMessage = 'Low - Framework participation needs improvement'; } else { participationMessage = 'Critical - Framework rarely providing guidance'; } document.getElementById('participation-message').textContent = participationMessage; // Breakdown by service const participationByService = {}; frameworkBackedDecisions.forEach(d => { const service = d.service || 'Unknown'; if (!participationByService[service]) { participationByService[service] = 0; } participationByService[service]++; }); const participationBreakdownEl = document.getElementById('participation-breakdown'); const sortedServices = Object.entries(participationByService) .sort((a, b) => b[1] - a[1]) .slice(0, 5); // Top 5 services if (sortedServices.length > 0) { participationBreakdownEl.innerHTML = sortedServices .map(([service, count]) => { const percentage = ((count / frameworkBackedDecisions.length) * 100).toFixed(0); return `
${service}: ${count} (${percentage}%)
`; }) .join(''); } else { participationBreakdownEl.innerHTML = '
No framework guidance data yet
'; } // Team Comparison (AI vs Human) // Use same framework services list defined above const aiDecisions = auditData.filter(d => frameworkServices.includes(d.service) ); const humanDecisions = auditData.filter(d => !frameworkServices.includes(d.service) && d.service && d.service !== 'unknown' ); const comparisonEl = document.getElementById('team-comparison'); comparisonEl.innerHTML = ''; [ { label: 'AI Assistant', data: aiDecisions, color: 'blue' }, { label: 'Human Direct', data: humanDecisions, color: 'purple' } ].forEach(team => { const blocked = team.data.filter(d => !d.allowed).length; const blockRate = team.data.length > 0 ? (blocked / team.data.length * 100).toFixed(1) : '0.0'; const teamDiv = document.createElement('div'); teamDiv.className = `border-2 border-${team.color}-200 rounded-lg p-4`; teamDiv.innerHTML = `
${team.label}
${blockRate}%
Block Rate (${blocked}/${team.data.length})
${blocked === 0 ? 'โœ“ Clean performance' : `${blocked} violations prevented`}
`; comparisonEl.appendChild(teamDiv); }); // ROI Projections const roiEl = document.getElementById('roi-projections'); roiEl.innerHTML = ''; const blockedCount = auditData.filter(d => !d.allowed).length; const highSeverityCount = auditData.filter(d => !d.allowed && d.violations && d.violations.some(v => v.severity === 'HIGH' || v.severity === 'CRITICAL') ).length; const projections = [ { users: '1,000', decisions: Math.round(auditData.length * 10), blocks: Math.round(blockedCount * 10), critical: Math.round(highSeverityCount * 10) }, { users: '10,000', decisions: Math.round(auditData.length * 100), blocks: Math.round(blockedCount * 100), critical: Math.round(highSeverityCount * 100) }, { users: '70,000', decisions: Math.round(auditData.length * 700), blocks: Math.round(blockedCount * 700), critical: Math.round(highSeverityCount * 700) } ]; projections.forEach(proj => { const projDiv = document.createElement('div'); projDiv.className = 'border-2 border-purple-200 rounded-lg p-4 bg-white'; projDiv.innerHTML = `
${proj.users}
Users
Decisions/month: ${proj.decisions.toLocaleString()}
Blocks/month: ${proj.blocks.toLocaleString()}
Critical saves: ${proj.critical.toLocaleString()}
`; roiEl.appendChild(projDiv); }); } // Render Framework Saves (High severity blocks) function renderFrameworkSaves() { const blockedDecisions = auditData.filter(d => !d.allowed); const highSeverityBlocks = blockedDecisions.filter(d => d.violations && d.violations.some(v => v.severity === 'HIGH' || v.severity === 'CRITICAL') ); document.getElementById('framework-saves-count').textContent = highSeverityBlocks.length; const savesListEl = document.getElementById('framework-saves-list'); if (highSeverityBlocks.length === 0) { savesListEl.innerHTML = '

No high-severity blocks in this period

'; return; } savesListEl.innerHTML = ''; highSeverityBlocks.slice(0, 10).forEach(block => { const violation = block.violations.find(v => v.severity === 'HIGH' || v.severity === 'CRITICAL'); const timestamp = new Date(block.timestamp).toLocaleString(); const file = block.metadata?.filePath || block.metadata?.file || 'N/A'; const isCritical = violation.severity === 'CRITICAL'; const saveDiv = document.createElement('div'); saveDiv.className = `flex items-start p-3 bg-white rounded-lg border ${isCritical ? 'border-red-200' : 'border-orange-200'} hover:shadow-md transition`; const badge = document.createElement('span'); badge.className = `inline-flex items-center px-2 py-1 text-xs font-medium rounded-full ${isCritical ? 'bg-red-100 text-red-800' : 'bg-orange-100 text-orange-800'}`; badge.textContent = violation.severity; const badgeContainer = document.createElement('div'); badgeContainer.className = 'flex-shrink-0 mr-3'; badgeContainer.appendChild(badge); const content = document.createElement('div'); content.className = 'flex-1 min-w-0'; content.innerHTML = `

${violation.ruleId || 'Unknown Rule'}

${violation.details || violation.ruleText || 'No details'}

File: ${file}

${timestamp}

`; saveDiv.appendChild(badgeContainer); saveDiv.appendChild(content); savesListEl.appendChild(saveDiv); }); } // Render Block Reasons function renderBlockReasons() { const blockedDecisions = auditData.filter(d => !d.allowed); const reasonCounts = {}; blockedDecisions.forEach(d => { let reason = 'Unknown'; if (d.violations && d.violations.length > 0) { reason = d.violations[0].ruleId || d.violations[0].ruleText || 'Unknown'; } else if (d.metadata?.reason) { reason = d.metadata.reason; } reasonCounts[reason] = (reasonCounts[reason] || 0) + 1; }); const sortedReasons = Object.entries(reasonCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 10); const reasonsListEl = document.getElementById('block-reasons-list'); if (sortedReasons.length === 0) { reasonsListEl.innerHTML = '

No blocks in this period

'; return; } const maxCount = sortedReasons[0][1]; reasonsListEl.innerHTML = ''; sortedReasons.forEach(([reason, count]) => { const percentage = (count / blockedDecisions.length * 100).toFixed(1); const barWidth = (count / maxCount * 100).toFixed(1); const container = document.createElement('div'); container.className = 'flex items-center space-x-3'; const innerDiv = document.createElement('div'); innerDiv.className = 'flex-1'; // Get rule description if available const ruleDesc = RULE_DESCRIPTIONS[reason]; const tooltip = ruleDesc ? `${ruleDesc.title}\n\n${ruleDesc.description}` : reason; const header = document.createElement('div'); header.className = 'flex items-center justify-between mb-1'; const reasonSpan = document.createElement('span'); reasonSpan.className = 'text-sm font-medium text-gray-700 truncate cursor-help'; reasonSpan.title = tooltip; reasonSpan.textContent = reason; // Add info icon for rules with descriptions if (ruleDesc) { const infoIcon = document.createElement('span'); infoIcon.className = 'ml-2 text-blue-500 text-xs'; infoIcon.textContent = 'โ„น๏ธ'; infoIcon.title = tooltip; reasonSpan.appendChild(infoIcon); } const countSpan = document.createElement('span'); countSpan.className = 'text-sm text-gray-600'; countSpan.textContent = `${count} (${percentage}%)`; header.appendChild(reasonSpan); header.appendChild(countSpan); const progressBg = document.createElement('div'); progressBg.className = 'w-full bg-gray-200 rounded-full h-2'; const progressBar = document.createElement('div'); progressBar.className = 'bg-red-600 h-2 rounded-full'; progressBar.style.width = `${barWidth}%`; progressBg.appendChild(progressBar); innerDiv.appendChild(header); innerDiv.appendChild(progressBg); container.appendChild(innerDiv); reasonsListEl.appendChild(container); }); } // Render Severity Breakdown function renderSeverityBreakdown() { const blockedDecisions = auditData.filter(d => !d.allowed); const severityCounts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 }; blockedDecisions.forEach(d => { if (d.violations && d.violations.length > 0) { d.violations.forEach(v => { const severity = v.severity || 'MEDIUM'; severityCounts[severity] = (severityCounts[severity] || 0) + 1; }); } }); const severityEl = document.getElementById('severity-breakdown'); const totalViolations = Object.values(severityCounts).reduce((a, b) => a + b, 0); if (totalViolations === 0) { severityEl.innerHTML = '

No severity data available

'; return; } const severityConfig = { CRITICAL: { color: 'red', icon: '๐Ÿ”ด', bg: 'bg-red-50', border: 'border-red-200', text: 'text-red-700' }, HIGH: { color: 'orange', icon: '๐ŸŸ ', bg: 'bg-orange-50', border: 'border-orange-200', text: 'text-orange-700' }, MEDIUM: { color: 'yellow', icon: '๐ŸŸก', bg: 'bg-yellow-50', border: 'border-yellow-200', text: 'text-yellow-700' }, LOW: { color: 'gray', icon: 'โšช', bg: 'bg-gray-50', border: 'border-gray-200', text: 'text-gray-700' } }; severityEl.innerHTML = ''; Object.entries(severityCounts).forEach(([severity, count]) => { const percentage = (count / totalViolations * 100).toFixed(1); const config = severityConfig[severity]; const div = document.createElement('div'); div.className = `flex items-center justify-between p-3 ${config.bg} rounded-lg border ${config.border}`; div.innerHTML = `
${config.icon} ${severity}
${count}
${percentage}%
`; severityEl.appendChild(div); }); } // 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}`; } // Cost Configuration async function loadCostConfig() { try { const token = getAuthToken(); const response = await fetch('/api/admin/cost-config', { headers: { 'Authorization': `Bearer ${token}` } }); if (!response.ok) { throw new Error('Failed to load cost config'); } const data = await response.json(); return data.costFactors; } catch (error) { console.error('Error loading cost config:', error); return null; } } async function saveCostConfig(costFactors) { try { const token = getAuthToken(); const response = await fetch('/api/admin/cost-config', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ costFactors }) }); if (!response.ok) { throw new Error('Failed to save cost config'); } return await response.json(); } catch (error) { console.error('Error saving cost config:', error); throw error; } } async function showCostConfigModal() { const costFactors = await loadCostConfig(); if (!costFactors) { alert('Failed to load cost configuration'); return; } // Slider ranges by severity const sliderConfig = { CRITICAL: { min: 1000, max: 250000, step: 1000 }, HIGH: { min: 500, max: 50000, step: 500 }, MEDIUM: { min: 100, max: 10000, step: 100 }, LOW: { min: 50, max: 5000, step: 50 } }; const modal = document.createElement('div'); modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4'; modal.innerHTML = ` `; document.body.appendChild(modal); // Sync sliders with number inputs ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'].forEach(severity => { const slider = modal.querySelector(`#cost-${severity}-slider`); const input = modal.querySelector(`#cost-${severity}-amount`); const display = modal.querySelector(`#cost-${severity}-display`); // Slider changes update input and display slider.addEventListener('input', (e) => { const value = parseFloat(e.target.value); input.value = value; display.textContent = value.toLocaleString(); }); // Input changes update slider and display input.addEventListener('input', (e) => { const value = parseFloat(e.target.value) || 0; const config = sliderConfig[severity]; const clampedValue = Math.min(Math.max(value, config.min), config.max); slider.value = clampedValue; display.textContent = value.toLocaleString(); }); }); // Close on cancel modal.querySelector('#cancel-config-btn').addEventListener('click', () => { document.body.removeChild(modal); }); // Save configuration modal.querySelector('#save-config-btn').addEventListener('click', async () => { const updatedCostFactors = {}; ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'].forEach(severity => { updatedCostFactors[severity] = { amount: parseFloat(document.getElementById(`cost-${severity}-amount`).value) || 0, currency: document.getElementById(`cost-${severity}-currency`).value || 'USD', rationale: document.getElementById(`cost-${severity}-rationale`).value || '' }; }); try { await saveCostConfig(updatedCostFactors); alert('Cost configuration saved successfully!'); document.body.removeChild(modal); loadAuditData(); // Reload to show updated calculations } catch (error) { alert('Failed to save cost configuration. Please try again.'); } }); // Close on background click modal.addEventListener('click', (e) => { if (e.target === modal) { document.body.removeChild(modal); } }); } // 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!'); } // Setup cost configuration button const configCostsBtn = document.getElementById('configure-costs-btn'); if (configCostsBtn) { console.log('[Audit Analytics] Cost config button found, attaching event listener'); configCostsBtn.addEventListener('click', showCostConfigModal); } // Setup cost period selector const periodSelector = document.getElementById('cost-period-selector'); if (periodSelector) { console.log('[Audit Analytics] Cost period selector found, attaching event listener'); periodSelector.addEventListener('change', () => { console.log('[Audit Analytics] Period changed, recalculating cost avoidance...'); renderBusinessIntelligence(); }); } // Setup environment filter const environmentFilter = document.getElementById('environment-filter'); if (environmentFilter) { console.log('[Audit Analytics] Environment filter found, attaching event listener'); environmentFilter.addEventListener('change', () => { const env = environmentFilter.value; console.log(`[Audit Analytics] Environment changed to: ${env}, reloading data...`); loadAuditData(); }); } // 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); } }); // Show legend modal function showLegendModal() { const modal = document.getElementById('legend-modal'); const content = document.getElementById('legend-content'); // Populate legend content content.innerHTML = ''; Object.entries(RULE_DESCRIPTIONS).forEach(([ruleId, ruleInfo]) => { const ruleDiv = document.createElement('div'); ruleDiv.className = 'border-l-4 border-blue-500 pl-4 py-2'; const titleDiv = document.createElement('div'); titleDiv.className = 'font-semibold text-gray-900'; titleDiv.textContent = `${ruleId}: ${ruleInfo.title}`; const descDiv = document.createElement('div'); descDiv.className = 'text-sm text-gray-600 mt-1'; descDiv.textContent = ruleInfo.description; ruleDiv.appendChild(titleDiv); ruleDiv.appendChild(descDiv); content.appendChild(ruleDiv); }); modal.classList.remove('hidden'); } // Hide legend modal function hideLegendModal() { const modal = document.getElementById('legend-modal'); modal.classList.add('hidden'); } // Add event listeners for modal document.addEventListener('DOMContentLoaded', () => { const showBtn = document.getElementById('show-legend-btn'); const closeBtn = document.getElementById('close-legend-btn'); const modal = document.getElementById('legend-modal'); if (showBtn) { showBtn.addEventListener('click', showLegendModal); } if (closeBtn) { closeBtn.addEventListener('click', hideLegendModal); } // Close modal when clicking outside if (modal) { modal.addEventListener('click', (e) => { if (e.target === modal) { hideLegendModal(); } }); } });