tractatus/public/js/admin/hooks-dashboard.js
TheFlow 4b7e50962d fix(admin): Phase 1 - critical auth and navigation fixes
SUMMARY:
Fixed 3 broken admin pages (newsletter, hooks dashboard, migrator) and
standardized navigation links. These pages were completely non-functional
due to localStorage key mismatches.

CRITICAL FIXES:
1. newsletter-management.js:
   - token → admin_token (5 occurrences)
   - admin → admin_user (2 occurrences)
   - Now matches login.js localStorage keys

2. hooks-dashboard.js:
   - tractatus_admin_token → admin_token
   - Now uses correct auth token

3. claude-md-migrator.js:
   - auth_token → admin_token (2 occurrences)
   - Added missing apiRequest() helper function
   - Fixed logout to clear both admin_token and admin_user

NAVIGATION FIXES:
4. newsletter-management.html:
   - dashboard.html → /admin/dashboard.html (absolute path)

5. claude-md-migrator.html:
   - ../css/tailwind.css → /css/tailwind.css?v=1759833751 (absolute + version)
   - Added tractatus-theme.min.css

BEFORE (BROKEN):
- Newsletter Management:  Auth failed (wrong token key)
- Hooks Dashboard:  Auth failed (wrong token key)
- CLAUDE.md Migrator:  Auth failed + missing apiRequest()

AFTER (WORKING):
- Newsletter Management:  Auth works, all API calls function
- Hooks Dashboard:  Auth works, metrics load
- CLAUDE.md Migrator:  Auth works, API requests function

NEXT STEPS (Phase 2):
- Create unified admin navbar component
- Standardize CSS versioning across all pages
- Verify/create missing API endpoints

🤖 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 21:33:50 +13:00

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('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);
}