/**
* 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 = `
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}
`;
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 `
`;
}).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 `
`;
}).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 `
`;
}).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 ? '...' : ''}
View
`;
}).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 = `
Configure Cost Factors
Set organizational cost values for different violation severities
${['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'].map(severity => {
const config = sliderConfig[severity];
const amount = Math.min(Math.max(costFactors[severity].amount, config.min), config.max);
return `
`}).join('')}
Note: These values are illustrative placeholders for research purposes.
Organizations should determine appropriate values based on their incident cost data.
Cancel
Save Configuration
`;
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();
}
});
}
});