Update project dependencies, documentation, and supporting files: - i18n improvements for multilingual support - Admin dashboard enhancements - Documentation updates for Koha/Stripe and deployment - Server middleware and model updates - Package dependency updates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
212 lines
7.5 KiB
JavaScript
212 lines
7.5 KiB
JavaScript
/**
|
|
* Hooks Dashboard
|
|
* Real-time monitoring of framework enforcement hooks
|
|
*/
|
|
|
|
const API_BASE = window.location.hostname === 'localhost' ? 'http://localhost:9000/api' : '/api';
|
|
|
|
// Load metrics on page load
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadMetrics();
|
|
|
|
// Auto-refresh every 30 seconds
|
|
setInterval(loadMetrics, 30000);
|
|
|
|
// Manual refresh button
|
|
document.getElementById('refresh-btn')?.addEventListener('click', loadMetrics);
|
|
});
|
|
|
|
/**
|
|
* Load hook metrics
|
|
*/
|
|
async function loadMetrics() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/admin/hooks/metrics`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${localStorage.getItem('tractatus_admin_token')}`
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to load metrics');
|
|
}
|
|
|
|
const data = await response.json();
|
|
displayMetrics(data);
|
|
} catch (error) {
|
|
console.error('Error loading metrics:', error);
|
|
showError('Failed to load hook metrics');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Display metrics in UI
|
|
*/
|
|
function displayMetrics(data) {
|
|
const metrics = data.metrics || {};
|
|
const executions = metrics.hook_executions || [];
|
|
const blocks = metrics.blocks || [];
|
|
const stats = metrics.session_stats || {};
|
|
|
|
// Calculate totals
|
|
const totalExecutions = executions.length;
|
|
const totalBlocks = blocks.length;
|
|
const blockRate = totalExecutions > 0 ? ((totalBlocks / totalExecutions) * 100).toFixed(1) + '%' : '0%';
|
|
|
|
// Update quick stats
|
|
document.getElementById('stat-total-executions').textContent = totalExecutions.toLocaleString();
|
|
document.getElementById('stat-total-blocks').textContent = totalBlocks.toLocaleString();
|
|
document.getElementById('stat-block-rate').textContent = blockRate;
|
|
|
|
// Last updated
|
|
const lastUpdated = stats.last_updated ? formatRelativeTime(new Date(stats.last_updated)) : 'Never';
|
|
document.getElementById('stat-last-updated').textContent = lastUpdated;
|
|
|
|
// Hook breakdown
|
|
const editExecutions = executions.filter(e => e.hook === 'validate-file-edit').length;
|
|
const editBlocks = blocks.filter(b => b.hook === 'validate-file-edit').length;
|
|
const editSuccessRate = editExecutions > 0 ? (((editExecutions - editBlocks) / editExecutions) * 100).toFixed(1) + '%' : '100%';
|
|
|
|
document.getElementById('edit-executions').textContent = editExecutions.toLocaleString();
|
|
document.getElementById('edit-blocks').textContent = editBlocks.toLocaleString();
|
|
document.getElementById('edit-success-rate').textContent = editSuccessRate;
|
|
|
|
const writeExecutions = executions.filter(e => e.hook === 'validate-file-write').length;
|
|
const writeBlocks = blocks.filter(b => b.hook === 'validate-file-write').length;
|
|
const writeSuccessRate = writeExecutions > 0 ? (((writeExecutions - writeBlocks) / writeExecutions) * 100).toFixed(1) + '%' : '100%';
|
|
|
|
document.getElementById('write-executions').textContent = writeExecutions.toLocaleString();
|
|
document.getElementById('write-blocks').textContent = writeBlocks.toLocaleString();
|
|
document.getElementById('write-success-rate').textContent = writeSuccessRate;
|
|
|
|
// Recent blocks
|
|
displayRecentBlocks(blocks.slice(-10).reverse());
|
|
|
|
// Recent activity
|
|
displayRecentActivity(executions.slice(-20).reverse());
|
|
}
|
|
|
|
/**
|
|
* Display recent blocked operations
|
|
*/
|
|
function displayRecentBlocks(blocks) {
|
|
const container = document.getElementById('recent-blocks');
|
|
|
|
if (blocks.length === 0) {
|
|
container.innerHTML = '<div class="text-center py-8 text-gray-500">No blocked operations</div>';
|
|
return;
|
|
}
|
|
|
|
const html = blocks.map(block => `
|
|
<div class="py-4 border-b border-gray-100 last:border-0">
|
|
<div class="flex items-start gap-4">
|
|
<div class="flex-shrink-0 p-2 bg-red-100 rounded-lg">
|
|
<svg class="h-5 w-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"/>
|
|
</svg>
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-center gap-2 mb-1">
|
|
<span class="text-xs font-semibold px-2 py-0.5 bg-red-100 text-red-700 rounded">${block.hook.replace('validate-file-', '')}</span>
|
|
<span class="text-xs text-gray-500">${formatRelativeTime(new Date(block.timestamp))}</span>
|
|
</div>
|
|
<p class="text-sm font-medium text-gray-900 truncate">${escapeHtml(block.file)}</p>
|
|
<p class="text-sm text-red-600 mt-1">${escapeHtml(block.reason)}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
/**
|
|
* Display recent hook executions
|
|
*/
|
|
function displayRecentActivity(executions) {
|
|
const container = document.getElementById('recent-activity');
|
|
|
|
if (executions.length === 0) {
|
|
container.innerHTML = '<div class="text-center py-8 text-gray-500">No recent activity</div>';
|
|
return;
|
|
}
|
|
|
|
const html = executions.map(exec => `
|
|
<div class="py-3 border-b border-gray-100 last:border-0">
|
|
<div class="flex items-center gap-3">
|
|
<div class="flex-shrink-0">
|
|
${exec.result === 'passed' ? `
|
|
<svg class="h-5 w-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
</svg>
|
|
` : `
|
|
<svg class="h-5 w-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
</svg>
|
|
`}
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-xs font-semibold px-2 py-0.5 bg-gray-100 text-gray-700 rounded">${exec.hook.replace('validate-file-', '')}</span>
|
|
<span class="text-xs text-gray-500">${formatRelativeTime(new Date(exec.timestamp))}</span>
|
|
</div>
|
|
<p class="text-sm text-gray-900 truncate mt-1">${escapeHtml(exec.file)}</p>
|
|
${exec.reason ? `<p class="text-xs text-gray-500 mt-1">${escapeHtml(exec.reason)}</p>` : ''}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
/**
|
|
* Format relative time
|
|
*/
|
|
function formatRelativeTime(date) {
|
|
const now = new Date();
|
|
const diffMs = now - date;
|
|
const diffSec = Math.floor(diffMs / 1000);
|
|
const diffMin = Math.floor(diffSec / 60);
|
|
const diffHour = Math.floor(diffMin / 60);
|
|
const diffDay = Math.floor(diffHour / 24);
|
|
|
|
if (diffSec < 60) {
|
|
return 'Just now';
|
|
} else if (diffMin < 60) {
|
|
return `${diffMin}m ago`;
|
|
} else if (diffHour < 24) {
|
|
return `${diffHour}h ago`;
|
|
} else if (diffDay < 7) {
|
|
return `${diffDay}d ago`;
|
|
} else {
|
|
return date.toLocaleDateString();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Escape HTML to prevent XSS
|
|
*/
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
/**
|
|
* Show error message
|
|
*/
|
|
function showError(message) {
|
|
const container = document.getElementById('recent-activity');
|
|
container.innerHTML = `
|
|
<div class="text-center py-8">
|
|
<p class="text-red-600">${escapeHtml(message)}</p>
|
|
<button id="retry-load-btn" class="mt-4 text-sm text-blue-600 hover:text-blue-700">
|
|
Try Again
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
// Add event listener to retry button
|
|
document.getElementById('retry-load-btn')?.addEventListener('click', loadMetrics);
|
|
}
|