feat: enhance hooks with metrics tracking and admin dashboard
Implements comprehensive monitoring and fixes hook execution issues. Hook Validator Enhancements: - Fixed stdin JSON input reading (was using argv, now reads from stdin) - Changed exit codes from 1 to 2 for proper blocking (Claude Code spec) - Added metrics logging to all validators (Edit and Write hooks) - Metrics track: executions, blocks, success rates, timestamps Admin Dashboard: - Created /admin/hooks-dashboard.html - Real-time metrics visualization - Shows: total executions, blocks, block rates, hook breakdown - Displays recent blocked operations and activity feed - Auto-refreshes every 30 seconds API Integration: - Created /api/admin/hooks/metrics endpoint - Serves metrics.json to admin dashboard - Protected by admin authentication middleware Metrics Storage: - Created .claude/metrics/hooks-metrics.json - Tracks last 1000 executions, 500 blocks - Session stats: total hooks, blocks, last updated - Proven working: 11 hook executions logged during implementation Bug Fix: - Resolved "non-blocking status code 1" issue - Hooks now properly receive tool parameters via stdin JSON - Exit code 2 properly blocks operations per Claude Code spec Impact: - Framework enforcement is now observable and measurable - Admin can monitor hook effectiveness in real-time - Validates architectural enforcement approach 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
f442a8c398
commit
b932ee2e2a
8 changed files with 740 additions and 17 deletions
87
.claude/metrics/hooks-metrics.json
Normal file
87
.claude/metrics/hooks-metrics.json
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
{
|
||||
"hook_executions": [
|
||||
{
|
||||
"hook": "validate-file-edit",
|
||||
"timestamp": "2025-10-15T07:15:08.552Z",
|
||||
"file": "/home/theflow/projects/tractatus/scripts/hook-validators/validate-file-edit.js",
|
||||
"result": "passed",
|
||||
"reason": null
|
||||
},
|
||||
{
|
||||
"hook": "validate-file-edit",
|
||||
"timestamp": "2025-10-15T07:15:08.876Z",
|
||||
"file": "/home/theflow/projects/tractatus/scripts/hook-validators/validate-file-edit.js",
|
||||
"result": "passed",
|
||||
"reason": null
|
||||
},
|
||||
{
|
||||
"hook": "validate-file-edit",
|
||||
"timestamp": "2025-10-15T07:15:09.096Z",
|
||||
"file": "/home/theflow/projects/tractatus/scripts/hook-validators/validate-file-edit.js",
|
||||
"result": "passed",
|
||||
"reason": null
|
||||
},
|
||||
{
|
||||
"hook": "validate-file-edit",
|
||||
"timestamp": "2025-10-15T07:15:19.424Z",
|
||||
"file": "/home/theflow/projects/tractatus/scripts/hook-validators/validate-file-edit.js",
|
||||
"result": "passed",
|
||||
"reason": null
|
||||
},
|
||||
{
|
||||
"hook": "validate-file-edit",
|
||||
"timestamp": "2025-10-15T07:15:32.522Z",
|
||||
"file": "/home/theflow/projects/tractatus/scripts/hook-validators/validate-file-write.js",
|
||||
"result": "passed",
|
||||
"reason": null
|
||||
},
|
||||
{
|
||||
"hook": "validate-file-edit",
|
||||
"timestamp": "2025-10-15T07:16:10.450Z",
|
||||
"file": "/home/theflow/projects/tractatus/scripts/hook-validators/validate-file-write.js",
|
||||
"result": "passed",
|
||||
"reason": null
|
||||
},
|
||||
{
|
||||
"hook": "validate-file-edit",
|
||||
"timestamp": "2025-10-15T07:16:10.704Z",
|
||||
"file": "/home/theflow/projects/tractatus/scripts/hook-validators/validate-file-write.js",
|
||||
"result": "passed",
|
||||
"reason": null
|
||||
},
|
||||
{
|
||||
"hook": "validate-file-edit",
|
||||
"timestamp": "2025-10-15T07:16:11.024Z",
|
||||
"file": "/home/theflow/projects/tractatus/scripts/hook-validators/validate-file-write.js",
|
||||
"result": "passed",
|
||||
"reason": null
|
||||
},
|
||||
{
|
||||
"hook": "validate-file-edit",
|
||||
"timestamp": "2025-10-15T07:16:45.763Z",
|
||||
"file": "/home/theflow/projects/tractatus/src/routes/index.js",
|
||||
"result": "passed",
|
||||
"reason": null
|
||||
},
|
||||
{
|
||||
"hook": "validate-file-edit",
|
||||
"timestamp": "2025-10-15T07:16:46.053Z",
|
||||
"file": "/home/theflow/projects/tractatus/src/routes/index.js",
|
||||
"result": "passed",
|
||||
"reason": null
|
||||
},
|
||||
{
|
||||
"hook": "validate-file-edit",
|
||||
"timestamp": "2025-10-15T07:16:46.366Z",
|
||||
"file": "/home/theflow/projects/tractatus/public/admin/dashboard.html",
|
||||
"result": "passed",
|
||||
"reason": null
|
||||
}
|
||||
],
|
||||
"blocks": [],
|
||||
"session_stats": {
|
||||
"total_edit_hooks": 11,
|
||||
"total_edit_blocks": 0,
|
||||
"last_updated": "2025-10-15T07:16:46.366Z"
|
||||
}
|
||||
}
|
||||
|
|
@ -30,6 +30,7 @@
|
|||
<a href="/admin/rule-manager.html" class="nav-link px-3 py-2 rounded-md text-sm font-medium bg-indigo-50 text-indigo-700 hover:bg-indigo-100">🔧 Rule Manager</a>
|
||||
<a href="/admin/blog-curation.html" class="nav-link px-3 py-2 rounded-md text-sm font-medium">Blog Curation</a>
|
||||
<a href="/admin/audit-analytics.html" class="nav-link px-3 py-2 rounded-md text-sm font-medium bg-purple-50 text-purple-700 hover:bg-purple-100">📊 Audit Analytics</a>
|
||||
<a href="/admin/hooks-dashboard.html" class="nav-link px-3 py-2 rounded-md text-sm font-medium bg-green-50 text-green-700 hover:bg-green-100">🔒 Hooks Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
|
|
|
|||
202
public/admin/hooks-dashboard.html
Normal file
202
public/admin/hooks-dashboard.html
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Framework Hooks Dashboard | Tractatus Admin</title>
|
||||
<link rel="stylesheet" href="/css/tailwind.css">
|
||||
<script src="/js/admin/auth-check.js"></script>
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="bg-white border-b border-gray-200">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 flex items-center">
|
||||
<div class="h-8 w-8 bg-green-600 rounded-lg flex items-center justify-center">
|
||||
<svg aria-hidden="true" class="h-5 w-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="ml-3 text-xl font-bold text-gray-900">Framework Hooks Dashboard</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="/admin/dashboard.html" class="text-sm text-gray-600 hover:text-gray-900">
|
||||
← Back to Dashboard
|
||||
</a>
|
||||
<span id="admin-name" class="text-sm text-gray-600"></span>
|
||||
<button id="logout-btn" class="text-sm font-medium text-gray-700 hover:text-gray-900">
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900">Framework Enforcement Metrics</h1>
|
||||
<p class="mt-2 text-gray-600">Real-time monitoring of Claude Code hook validators and architectural enforcement</p>
|
||||
</div>
|
||||
|
||||
<!-- Quick Stats -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<!-- Total Executions -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 bg-blue-100 rounded-md p-3">
|
||||
<svg aria-hidden="true" class="h-6 w-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500">Total Hook Executions</p>
|
||||
<p id="stat-total-executions" class="text-2xl font-semibold text-gray-900">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Total Blocks -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 bg-red-100 rounded-md p-3">
|
||||
<svg aria-hidden="true" class="h-6 w-6 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="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500">Operations Blocked</p>
|
||||
<p id="stat-total-blocks" class="text-2xl font-semibold text-gray-900">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Block Rate -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 bg-yellow-100 rounded-md p-3">
|
||||
<svg aria-hidden="true" class="h-6 w-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500">Block Rate</p>
|
||||
<p id="stat-block-rate" class="text-2xl font-semibold text-gray-900">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Last Updated -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 bg-green-100 rounded-md p-3">
|
||||
<svg aria-hidden="true" class="h-6 w-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500">Last Activity</p>
|
||||
<p id="stat-last-updated" class="text-sm font-semibold text-gray-900">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hook Breakdown -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
<!-- Edit Hook Stats -->
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900 flex items-center gap-2">
|
||||
<svg class="h-5 w-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
||||
</svg>
|
||||
Edit Hook
|
||||
</h3>
|
||||
</div>
|
||||
<div class="px-6 py-4">
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-600">Total Executions:</span>
|
||||
<span id="edit-executions" class="text-sm font-semibold text-gray-900">-</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-600">Blocks:</span>
|
||||
<span id="edit-blocks" class="text-sm font-semibold text-red-600">-</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-600">Success Rate:</span>
|
||||
<span id="edit-success-rate" class="text-sm font-semibold text-green-600">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Write Hook Stats -->
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900 flex items-center gap-2">
|
||||
<svg class="h-5 w-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
Write Hook
|
||||
</h3>
|
||||
</div>
|
||||
<div class="px-6 py-4">
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-600">Total Executions:</span>
|
||||
<span id="write-executions" class="text-sm font-semibold text-gray-900">-</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-600">Blocks:</span>
|
||||
<span id="write-blocks" class="text-sm font-semibold text-red-600">-</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-600">Success Rate:</span>
|
||||
<span id="write-success-rate" class="text-sm font-semibold text-green-600">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Blocks -->
|
||||
<div class="bg-white rounded-lg shadow mb-8">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">Recent Blocked Operations</h3>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-200">
|
||||
<div id="recent-blocks" class="px-6 py-4">
|
||||
<div class="text-center py-8 text-gray-500">No blocked operations</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
|
||||
<h3 class="text-lg font-medium text-gray-900">Recent Hook Executions</h3>
|
||||
<button id="refresh-btn" class="text-sm text-blue-600 hover:text-blue-700 font-medium">
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-200">
|
||||
<div id="recent-activity" class="px-6 py-4 max-h-96 overflow-y-auto">
|
||||
<div class="text-center py-8 text-gray-500">Loading activity...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/js/admin/hooks-dashboard.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
209
public/js/admin/hooks-dashboard.js
Normal file
209
public/js/admin/hooks-dashboard.js
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
/**
|
||||
* 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 onclick="loadMetrics()" class="mt-4 text-sm text-blue-600 hover:text-blue-700">
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
@ -23,7 +23,10 @@ const fs = require('fs');
|
|||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
const FILE_PATH = process.argv[2];
|
||||
// Hooks receive input via stdin as JSON
|
||||
let HOOK_INPUT = null;
|
||||
let FILE_PATH = null;
|
||||
|
||||
const SESSION_STATE_PATH = path.join(__dirname, '../../.claude/session-state.json');
|
||||
const INSTRUCTION_HISTORY_PATH = path.join(__dirname, '../../.claude/instruction-history.json');
|
||||
|
||||
|
|
@ -177,15 +180,99 @@ function updateSessionState() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log metrics for hook execution
|
||||
*/
|
||||
function logMetrics(result, reason = null) {
|
||||
try {
|
||||
const METRICS_PATH = path.join(__dirname, '../../.claude/metrics/hooks-metrics.json');
|
||||
const METRICS_DIR = path.dirname(METRICS_PATH);
|
||||
|
||||
// Ensure directory exists
|
||||
if (!fs.existsSync(METRICS_DIR)) {
|
||||
fs.mkdirSync(METRICS_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// Load existing metrics
|
||||
let metrics = { hook_executions: [], blocks: [], session_stats: {} };
|
||||
if (fs.existsSync(METRICS_PATH)) {
|
||||
metrics = JSON.parse(fs.readFileSync(METRICS_PATH, 'utf8'));
|
||||
}
|
||||
|
||||
// Log execution
|
||||
metrics.hook_executions.push({
|
||||
hook: 'validate-file-edit',
|
||||
timestamp: new Date().toISOString(),
|
||||
file: FILE_PATH,
|
||||
result: result,
|
||||
reason: reason
|
||||
});
|
||||
|
||||
// Log block if failed
|
||||
if (result === 'blocked') {
|
||||
metrics.blocks.push({
|
||||
hook: 'validate-file-edit',
|
||||
timestamp: new Date().toISOString(),
|
||||
file: FILE_PATH,
|
||||
reason: reason
|
||||
});
|
||||
}
|
||||
|
||||
// Update session stats
|
||||
metrics.session_stats.total_edit_hooks = (metrics.session_stats.total_edit_hooks || 0) + 1;
|
||||
metrics.session_stats.total_edit_blocks = (metrics.session_stats.total_edit_blocks || 0) + (result === 'blocked' ? 1 : 0);
|
||||
metrics.session_stats.last_updated = new Date().toISOString();
|
||||
|
||||
// Keep only last 1000 executions
|
||||
if (metrics.hook_executions.length > 1000) {
|
||||
metrics.hook_executions = metrics.hook_executions.slice(-1000);
|
||||
}
|
||||
|
||||
// Keep only last 500 blocks
|
||||
if (metrics.blocks.length > 500) {
|
||||
metrics.blocks = metrics.blocks.slice(-500);
|
||||
}
|
||||
|
||||
// Write metrics
|
||||
fs.writeFileSync(METRICS_PATH, JSON.stringify(metrics, null, 2));
|
||||
} catch (err) {
|
||||
// Non-critical - don't fail on metrics logging
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read hook input from stdin
|
||||
*/
|
||||
function readHookInput() {
|
||||
return new Promise((resolve) => {
|
||||
let data = '';
|
||||
process.stdin.on('data', chunk => {
|
||||
data += chunk;
|
||||
});
|
||||
process.stdin.on('end', () => {
|
||||
try {
|
||||
resolve(JSON.parse(data));
|
||||
} catch (err) {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Main validation
|
||||
*/
|
||||
async function main() {
|
||||
if (!FILE_PATH) {
|
||||
error('No file path provided');
|
||||
process.exit(1);
|
||||
// Read input from stdin
|
||||
HOOK_INPUT = await readHookInput();
|
||||
|
||||
if (!HOOK_INPUT || !HOOK_INPUT.tool_input || !HOOK_INPUT.tool_input.file_path) {
|
||||
error('No file path provided in hook input');
|
||||
logMetrics('error', 'No file path in input');
|
||||
process.exit(2); // Exit code 2 = BLOCK
|
||||
}
|
||||
|
||||
FILE_PATH = HOOK_INPUT.tool_input.file_path;
|
||||
log(`\n🔍 Hook: Validating file edit: ${FILE_PATH}`, 'cyan');
|
||||
|
||||
// Check 1: Pre-action validation
|
||||
|
|
@ -195,7 +282,8 @@ async function main() {
|
|||
if (preCheck.output) {
|
||||
console.log(preCheck.output);
|
||||
}
|
||||
process.exit(1);
|
||||
logMetrics('blocked', preCheck.reason);
|
||||
process.exit(2); // Exit code 2 = BLOCK
|
||||
}
|
||||
success('Pre-action check passed');
|
||||
|
||||
|
|
@ -206,7 +294,8 @@ async function main() {
|
|||
conflicts.conflicts.forEach(c => {
|
||||
log(` • ${c.id}: ${c.instruction} [${c.quadrant}]`, 'yellow');
|
||||
});
|
||||
process.exit(1);
|
||||
logMetrics('blocked', conflicts.reason);
|
||||
process.exit(2); // Exit code 2 = BLOCK
|
||||
}
|
||||
success('No instruction conflicts detected');
|
||||
|
||||
|
|
@ -214,18 +303,23 @@ async function main() {
|
|||
const boundary = checkBoundaryViolation();
|
||||
if (!boundary.passed) {
|
||||
error(boundary.reason);
|
||||
process.exit(1);
|
||||
logMetrics('blocked', boundary.reason);
|
||||
process.exit(2); // Exit code 2 = BLOCK
|
||||
}
|
||||
success('No boundary violations detected');
|
||||
|
||||
// Update session state
|
||||
updateSessionState();
|
||||
|
||||
// Log successful execution
|
||||
logMetrics('passed');
|
||||
|
||||
success('File edit validation complete\n');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
error(`Hook validation error: ${err.message}`);
|
||||
process.exit(1);
|
||||
logMetrics('error', err.message);
|
||||
process.exit(2); // Exit code 2 = BLOCK
|
||||
});
|
||||
|
|
|
|||
|
|
@ -24,7 +24,10 @@ const fs = require('fs');
|
|||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
const FILE_PATH = process.argv[2];
|
||||
// Hooks receive input via stdin as JSON
|
||||
let HOOK_INPUT = null;
|
||||
let FILE_PATH = null;
|
||||
|
||||
const SESSION_STATE_PATH = path.join(__dirname, '../../.claude/session-state.json');
|
||||
const INSTRUCTION_HISTORY_PATH = path.join(__dirname, '../../.claude/instruction-history.json');
|
||||
|
||||
|
|
@ -195,12 +198,36 @@ function updateSessionState() {
|
|||
/**
|
||||
* Main validation
|
||||
*/
|
||||
/**
|
||||
* Read hook input from stdin
|
||||
*/
|
||||
function readHookInput() {
|
||||
return new Promise((resolve) => {
|
||||
let data = "";
|
||||
process.stdin.on("data", chunk => {
|
||||
data += chunk;
|
||||
});
|
||||
process.stdin.on("end", () => {
|
||||
try {
|
||||
resolve(JSON.parse(data));
|
||||
} catch (err) {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!FILE_PATH) {
|
||||
error('No file path provided');
|
||||
process.exit(1);
|
||||
// Read input from stdin
|
||||
HOOK_INPUT = await readHookInput();
|
||||
|
||||
if (!HOOK_INPUT || !HOOK_INPUT.tool_input || !HOOK_INPUT.tool_input.file_path) {
|
||||
logMetrics('error', 'No file path in input');
|
||||
error('No file path provided in hook input');
|
||||
process.exit(2); // Exit code 2 = BLOCK
|
||||
}
|
||||
|
||||
FILE_PATH = HOOK_INPUT.tool_input.file_path;
|
||||
log(`\n🔍 Hook: Validating file write: ${FILE_PATH}`, 'cyan');
|
||||
|
||||
// Check 1: Pre-action validation
|
||||
|
|
@ -210,7 +237,8 @@ async function main() {
|
|||
if (preCheck.output) {
|
||||
console.log(preCheck.output);
|
||||
}
|
||||
process.exit(1);
|
||||
logMetrics('blocked', preCheck.reason);
|
||||
process.exit(2); // Exit code 2 = BLOCK
|
||||
}
|
||||
success('Pre-action check passed');
|
||||
|
||||
|
|
@ -218,7 +246,8 @@ async function main() {
|
|||
const overwriteCheck = checkOverwriteWithoutRead();
|
||||
if (!overwriteCheck.passed) {
|
||||
error(overwriteCheck.reason);
|
||||
process.exit(1);
|
||||
logMetrics('blocked', overwriteCheck.reason);
|
||||
process.exit(2); // Exit code 2 = BLOCK
|
||||
}
|
||||
|
||||
// Check 3: CrossReferenceValidator
|
||||
|
|
@ -228,7 +257,8 @@ async function main() {
|
|||
conflicts.conflicts.forEach(c => {
|
||||
log(` • ${c.id}: ${c.instruction} [${c.quadrant}]`, 'yellow');
|
||||
});
|
||||
process.exit(1);
|
||||
logMetrics('blocked', conflicts.reason);
|
||||
process.exit(2); // Exit code 2 = BLOCK
|
||||
}
|
||||
success('No instruction conflicts detected');
|
||||
|
||||
|
|
@ -236,18 +266,67 @@ async function main() {
|
|||
const boundary = checkBoundaryViolation();
|
||||
if (!boundary.passed) {
|
||||
error(boundary.reason);
|
||||
process.exit(1);
|
||||
logMetrics('blocked', boundary.reason);
|
||||
process.exit(2); // Exit code 2 = BLOCK
|
||||
}
|
||||
success('No boundary violations detected');
|
||||
|
||||
// Update session state
|
||||
updateSessionState();
|
||||
|
||||
// Log successful execution
|
||||
logMetrics('passed');
|
||||
|
||||
success('File write validation complete\n');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
error(`Hook validation error: ${err.message}`);
|
||||
process.exit(1);
|
||||
logMetrics('error', err.message);
|
||||
process.exit(2); // Exit code 2 = BLOCK
|
||||
});
|
||||
|
||||
/**
|
||||
* Log metrics for hook execution
|
||||
*/
|
||||
function logMetrics(result, reason = null) {
|
||||
try {
|
||||
const METRICS_PATH = path.join(__dirname, '../../.claude/metrics/hooks-metrics.json');
|
||||
const METRICS_DIR = path.dirname(METRICS_PATH);
|
||||
if (!fs.existsSync(METRICS_DIR)) {
|
||||
fs.mkdirSync(METRICS_DIR, { recursive: true });
|
||||
}
|
||||
let metrics = { hook_executions: [], blocks: [], session_stats: {} };
|
||||
if (fs.existsSync(METRICS_PATH)) {
|
||||
metrics = JSON.parse(fs.readFileSync(METRICS_PATH, 'utf8'));
|
||||
}
|
||||
metrics.hook_executions.push({
|
||||
hook: 'validate-file-write',
|
||||
timestamp: new Date().toISOString(),
|
||||
file: FILE_PATH,
|
||||
result: result,
|
||||
reason: reason
|
||||
});
|
||||
if (result === 'blocked') {
|
||||
metrics.blocks.push({
|
||||
hook: 'validate-file-write',
|
||||
timestamp: new Date().toISOString(),
|
||||
file: FILE_PATH,
|
||||
reason: reason
|
||||
});
|
||||
}
|
||||
metrics.session_stats.total_write_hooks = (metrics.session_stats.total_write_hooks || 0) + 1;
|
||||
metrics.session_stats.total_write_blocks = (metrics.session_stats.total_write_blocks || 0) + (result === 'blocked' ? 1 : 0);
|
||||
metrics.session_stats.last_updated = new Date().toISOString();
|
||||
if (metrics.hook_executions.length > 1000) {
|
||||
metrics.hook_executions = metrics.hook_executions.slice(-1000);
|
||||
}
|
||||
if (metrics.blocks.length > 500) {
|
||||
metrics.blocks = metrics.blocks.slice(-500);
|
||||
}
|
||||
fs.writeFileSync(METRICS_PATH, JSON.stringify(metrics, null, 2));
|
||||
} catch (err) {
|
||||
// Non-critical
|
||||
}
|
||||
}
|
||||
|
|
|
|||
49
src/routes/hooks-metrics.routes.js
Normal file
49
src/routes/hooks-metrics.routes.js
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* Hooks Metrics API Routes
|
||||
* Serves framework enforcement metrics to admin dashboard
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { authMiddleware, roleMiddleware } = require('../middleware/auth.middleware');
|
||||
|
||||
const METRICS_PATH = path.join(__dirname, '../../.claude/metrics/hooks-metrics.json');
|
||||
|
||||
/**
|
||||
* GET /api/admin/hooks/metrics
|
||||
* Get current hooks metrics
|
||||
*/
|
||||
router.get('/metrics', authMiddleware, roleMiddleware(['admin']), async (req, res) => {
|
||||
try {
|
||||
// Check if metrics file exists
|
||||
if (!fs.existsSync(METRICS_PATH)) {
|
||||
return res.json({
|
||||
success: true,
|
||||
metrics: {
|
||||
hook_executions: [],
|
||||
blocks: [],
|
||||
session_stats: {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Read metrics
|
||||
const metricsData = fs.readFileSync(METRICS_PATH, 'utf8');
|
||||
const metrics = JSON.parse(metricsData);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
metrics: metrics
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching hooks metrics:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to fetch hooks metrics'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -14,6 +14,7 @@ const newsletterRoutes = require('./newsletter.routes');
|
|||
const mediaRoutes = require('./media.routes');
|
||||
const casesRoutes = require('./cases.routes');
|
||||
const adminRoutes = require('./admin.routes');
|
||||
const hooksMetricsRoutes = require('./hooks-metrics.routes');
|
||||
const rulesRoutes = require('./rules.routes');
|
||||
const projectsRoutes = require('./projects.routes');
|
||||
const auditRoutes = require('./audit.routes');
|
||||
|
|
@ -35,6 +36,7 @@ router.use('/newsletter', newsletterRoutes);
|
|||
router.use('/media', mediaRoutes);
|
||||
router.use('/cases', casesRoutes);
|
||||
router.use('/admin', adminRoutes);
|
||||
router.use('/admin/hooks', hooksMetricsRoutes);
|
||||
router.use('/admin/rules', rulesRoutes);
|
||||
router.use('/admin/projects', projectsRoutes);
|
||||
router.use('/admin', auditRoutes);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue