infinite-agents-public/claude_code_devtools/claude_devtool_10.html

1106 lines
42 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IndexedDB Event Store - Claude Code DevTools</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #21262d;
--border-color: #30363d;
--text-primary: #c9d1d9;
--text-secondary: #8b949e;
--accent-blue: #58a6ff;
--accent-green: #3fb950;
--accent-purple: #bc8cff;
--accent-orange: #ff9f43;
--accent-red: #f85149;
--accent-yellow: #f0e68c;
--shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
body {
font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
padding: 20px;
}
header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid var(--border-color);
}
h1 {
color: var(--accent-blue);
margin-bottom: 10px;
font-size: 2.5em;
}
.tagline {
color: var(--text-secondary);
font-size: 1.1em;
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.control-group {
background: var(--bg-secondary);
padding: 20px;
border-radius: 8px;
border: 1px solid var(--border-color);
}
.control-group h3 {
color: var(--accent-purple);
margin-bottom: 15px;
font-size: 1.1em;
}
button {
background: var(--accent-blue);
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
margin: 5px 5px 5px 0;
transition: all 0.2s;
}
button:hover {
background: #4a9eff;
transform: translateY(-1px);
box-shadow: var(--shadow);
}
button:disabled {
background: var(--text-secondary);
cursor: not-allowed;
opacity: 0.5;
}
button.secondary {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
}
button.danger {
background: var(--accent-red);
}
button.success {
background: var(--accent-green);
}
input, select, textarea {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
color: var(--text-primary);
padding: 8px 12px;
border-radius: 5px;
font-size: 14px;
width: 100%;
margin-bottom: 10px;
}
input:focus, select:focus, textarea:focus {
outline: none;
border-color: var(--accent-blue);
}
.status-bar {
background: var(--bg-secondary);
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 15px;
}
.status-item {
display: flex;
align-items: center;
gap: 8px;
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--text-secondary);
}
.status-indicator.active {
background: var(--accent-green);
box-shadow: 0 0 8px var(--accent-green);
}
.status-indicator.error {
background: var(--accent-red);
box-shadow: 0 0 8px var(--accent-red);
}
.query-panel {
background: var(--bg-secondary);
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.query-filters {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 15px;
}
.results-section {
background: var(--bg-secondary);
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.event-list {
max-height: 500px;
overflow-y: auto;
border: 1px solid var(--border-color);
border-radius: 5px;
}
.event-item {
padding: 15px;
border-bottom: 1px solid var(--border-color);
transition: background 0.2s;
}
.event-item:hover {
background: var(--bg-tertiary);
}
.event-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.event-type {
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
}
.event-type.PreToolUse { background: var(--accent-blue); color: white; }
.event-type.PostToolUse { background: var(--accent-green); color: white; }
.event-type.Notification { background: var(--accent-orange); color: white; }
.event-type.Stop { background: var(--accent-red); color: white; }
.event-type.UserPromptSubmit { background: var(--accent-purple); color: white; }
.event-meta {
display: flex;
gap: 20px;
color: var(--text-secondary);
font-size: 13px;
margin-top: 8px;
}
.event-summary {
color: var(--text-primary);
margin-top: 8px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-top: 20px;
}
.stat-card {
background: var(--bg-tertiary);
padding: 15px;
border-radius: 8px;
border: 1px solid var(--border-color);
}
.stat-value {
font-size: 2em;
color: var(--accent-blue);
font-weight: bold;
}
.stat-label {
color: var(--text-secondary);
font-size: 0.9em;
margin-top: 5px;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
margin-top: 15px;
}
.docs {
background: var(--bg-secondary);
padding: 25px;
border-radius: 8px;
margin-top: 30px;
}
.docs h2 {
color: var(--accent-blue);
margin-bottom: 20px;
}
.docs h3 {
color: var(--accent-purple);
margin: 20px 0 10px 0;
}
.docs ul, .docs ol {
margin-left: 25px;
margin-bottom: 15px;
}
.docs li {
margin-bottom: 8px;
}
.docs code {
background: var(--bg-tertiary);
padding: 2px 6px;
border-radius: 3px;
color: var(--accent-orange);
font-family: 'Courier New', monospace;
}
footer {
text-align: center;
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid var(--border-color);
color: var(--text-secondary);
}
footer a {
color: var(--accent-blue);
text-decoration: none;
}
footer a:hover {
text-decoration: underline;
}
.json-viewer {
background: var(--bg-tertiary);
padding: 10px;
border-radius: 5px;
font-family: 'Courier New', monospace;
font-size: 12px;
max-height: 200px;
overflow-y: auto;
margin-top: 10px;
white-space: pre-wrap;
word-break: break-all;
}
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: var(--bg-tertiary);
}
::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-secondary);
}
</style>
</head>
<body>
<header>
<h1>🗄️ IndexedDB Event Store</h1>
<p class="tagline">Production-grade persistent storage for Claude Code hook events</p>
</header>
<main>
<!-- Status Bar -->
<div class="status-bar">
<div class="status-item">
<div class="status-indicator" id="dbStatus"></div>
<span>Database: <strong id="dbStatusText">Initializing...</strong></span>
</div>
<div class="status-item">
<div class="status-indicator" id="wsStatus"></div>
<span>WebSocket: <strong id="wsStatusText">Disconnected</strong></span>
</div>
<div class="status-item">
<span>Events Stored: <strong id="eventCount">0</strong></span>
</div>
<div class="status-item">
<span>DB Size: <strong id="dbSize">0 KB</strong></span>
</div>
</div>
<!-- Controls -->
<div class="controls">
<div class="control-group">
<h3>🔌 WebSocket Connection</h3>
<input type="text" id="wsUrl" value="ws://localhost:4000/stream" placeholder="WebSocket URL">
<button id="connectBtn" class="success">Connect to Stream</button>
<button id="disconnectBtn" class="danger" disabled>Disconnect</button>
</div>
<div class="control-group">
<h3>📂 Import/Export</h3>
<input type="file" id="fileInput" accept=".json,.jsonl" style="margin-bottom: 10px;">
<button id="importBtn" class="secondary">Import Events</button>
<button id="exportBtn" class="secondary">Export All Events</button>
</div>
<div class="control-group">
<h3>🧹 Maintenance</h3>
<button id="clearDbBtn" class="danger">Clear Database</button>
<button id="sampleDataBtn" class="secondary">Load Sample Data</button>
<button id="refreshStatsBtn" class="secondary">Refresh Statistics</button>
</div>
</div>
<!-- Query Panel -->
<div class="query-panel">
<h3 style="color: var(--accent-purple); margin-bottom: 15px;">🔍 Advanced Query Interface</h3>
<div class="query-filters">
<div>
<label>Source App:</label>
<select id="filterApp">
<option value="">All Apps</option>
</select>
</div>
<div>
<label>Session ID:</label>
<select id="filterSession">
<option value="">All Sessions</option>
</select>
</div>
<div>
<label>Event Type:</label>
<select id="filterEventType">
<option value="">All Types</option>
<option value="PreToolUse">PreToolUse</option>
<option value="PostToolUse">PostToolUse</option>
<option value="Notification">Notification</option>
<option value="Stop">Stop</option>
<option value="UserPromptSubmit">UserPromptSubmit</option>
<option value="SubagentStop">SubagentStop</option>
<option value="PreCompact">PreCompact</option>
</select>
</div>
<div>
<label>Time Range:</label>
<select id="filterTimeRange">
<option value="">All Time</option>
<option value="1h">Last Hour</option>
<option value="24h">Last 24 Hours</option>
<option value="7d">Last 7 Days</option>
</select>
</div>
</div>
<button id="queryBtn">Execute Query</button>
<button id="resetFiltersBtn" class="secondary">Reset Filters</button>
</div>
<!-- Results -->
<div class="results-section">
<h3 style="color: var(--accent-green); margin-bottom: 15px;">📊 Query Results</h3>
<div class="event-list" id="eventList">
<div style="padding: 40px; text-align: center; color: var(--text-secondary);">
No events to display. Connect to WebSocket or import events to get started.
</div>
</div>
<div class="pagination">
<button id="prevPageBtn" disabled>← Previous</button>
<span id="pageInfo">Page 1 of 1</span>
<button id="nextPageBtn" disabled>Next →</button>
</div>
</div>
<!-- Statistics -->
<div class="results-section">
<h3 style="color: var(--accent-orange); margin-bottom: 15px;">📈 Event Statistics</h3>
<div class="stats-grid" id="statsGrid">
<!-- Stats populated dynamically -->
</div>
</div>
<!-- Documentation -->
<section class="docs">
<h2>About This Tool</h2>
<h3>Purpose</h3>
<p>Production-grade persistent storage system for Claude Code hook events using IndexedDB. Store, query, and analyze thousands of events with advanced filtering, compound indexes, and real-time updates from the observability server.</p>
<h3>Features</h3>
<ul>
<li><strong>IndexedDB Storage:</strong> Persistent local database with compound indexes for efficient queries</li>
<li><strong>Real-time Updates:</strong> Connect to WebSocket server (ws://localhost:4000/stream) for live event streaming</li>
<li><strong>Multi-Field Indexing:</strong> Compound indexes on (source_app, timestamp) and (session_id, event_type)</li>
<li><strong>Advanced Queries:</strong> Filter by app, session, event type, and time range with cursor-based pagination</li>
<li><strong>Event Aggregation:</strong> Statistics by app, session, event type, and time period</li>
<li><strong>Import/Export:</strong> Load events from JSON/JSONL files and export filtered results</li>
<li><strong>Database Management:</strong> Size monitoring, cleanup, and sample data generation</li>
<li><strong>Cursor-based Pagination:</strong> Efficiently iterate over large result sets without loading all data</li>
</ul>
<h3>Web Research Integration</h3>
<p><strong>Source:</strong> <a href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API" target="_blank">MDN - IndexedDB API</a></p>
<p><strong>IndexedDB Techniques Applied:</strong></p>
<ul>
<li><strong>Database Versioning & Migrations:</strong> Used <code>indexedDB.open()</code> with version management and <code>onupgradeneeded</code> event to create object stores and indexes dynamically</li>
<li><strong>Compound Indexes:</strong> Created multi-field indexes like <code>[source_app, timestamp]</code> and <code>[session_id, event_type]</code> for efficient filtering on multiple criteria simultaneously</li>
<li><strong>IDBKeyRange for Filtering:</strong> Leveraged <code>IDBKeyRange.bound()</code> and <code>IDBKeyRange.only()</code> for precise time-based and value-based filtering</li>
<li><strong>Cursor-based Iteration:</strong> Implemented <code>IDBCursor</code> with <code>openCursor()</code> for memory-efficient pagination over large datasets without loading all records</li>
<li><strong>Transaction Management:</strong> Used read/write transactions with proper scope limiting to ensure data integrity and performance</li>
</ul>
<h3>Usage</h3>
<ol>
<li><strong>Connect to WebSocket:</strong> Click "Connect to Stream" to receive real-time events from ws://localhost:4000/stream</li>
<li><strong>Import Events:</strong> Upload JSON/JSONL files containing hook events from the observability system</li>
<li><strong>Query Events:</strong> Use filters to search by app, session, event type, or time range</li>
<li><strong>View Statistics:</strong> Analyze event distribution across apps, sessions, and event types</li>
<li><strong>Export Results:</strong> Download filtered events as JSON for backup or analysis</li>
<li><strong>Pagination:</strong> Navigate through large result sets using Previous/Next buttons</li>
</ol>
<h3>Event Structure</h3>
<p>Events from the multi-agent observability system have this structure:</p>
<ul>
<li><code>id</code>: Auto-incrementing unique identifier</li>
<li><code>source_app</code>: Application that generated the event (e.g., "demo-agent")</li>
<li><code>session_id</code>: Unique session identifier</li>
<li><code>hook_event_type</code>: Type of hook event (PreToolUse, PostToolUse, etc.)</li>
<li><code>payload</code>: Event-specific data (tool name, inputs, outputs)</li>
<li><code>timestamp</code>: ISO 8601 timestamp</li>
<li><code>summary</code>: AI-generated summary of the event</li>
</ul>
<h3>IndexedDB Schema</h3>
<p>Database: <code>HookEventsDB</code>, Version: 1</p>
<p>Object Store: <code>events</code></p>
<ul>
<li>Key Path: <code>id</code> (auto-increment)</li>
<li>Index 1: <code>source_app</code></li>
<li>Index 2: <code>session_id</code></li>
<li>Index 3: <code>hook_event_type</code></li>
<li>Index 4: <code>timestamp</code></li>
<li>Index 5: <code>app_time</code> (compound: [source_app, timestamp])</li>
<li>Index 6: <code>session_type</code> (compound: [session_id, hook_event_type])</li>
</ul>
</section>
</main>
<footer>
<p>Claude Code DevTools | Generated via web-enhanced infinite loop</p>
<p>Web Source: <a href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API" target="_blank">https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API</a></p>
</footer>
<script>
// IndexedDB Configuration
const DB_NAME = 'HookEventsDB';
const DB_VERSION = 1;
const STORE_NAME = 'events';
const PAGE_SIZE = 50;
// Global state
let db = null;
let ws = null;
let currentPage = 1;
let totalPages = 1;
let currentQuery = {};
// Initialize IndexedDB with compound indexes
async function initDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = () => {
updateStatus('db', 'error', 'Failed');
reject(request.error);
};
request.onsuccess = () => {
db = request.result;
updateStatus('db', 'active', 'Connected');
resolve(db);
};
// Database upgrade/creation - create object store and indexes
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Create object store with auto-incrementing key
const store = db.createObjectStore(STORE_NAME, {
keyPath: 'id',
autoIncrement: true
});
// Single-field indexes
store.createIndex('source_app', 'source_app', { unique: false });
store.createIndex('session_id', 'session_id', { unique: false });
store.createIndex('hook_event_type', 'hook_event_type', { unique: false });
store.createIndex('timestamp', 'timestamp', { unique: false });
// Compound indexes for multi-field queries
store.createIndex('app_time', ['source_app', 'timestamp'], { unique: false });
store.createIndex('session_type', ['session_id', 'hook_event_type'], { unique: false });
console.log('IndexedDB schema created with compound indexes');
};
});
}
// Add event to database using transaction
async function addEvent(event) {
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
// Add timestamp if not present
if (!event.timestamp) {
event.timestamp = new Date().toISOString();
}
const request = store.add(event);
request.onsuccess = () => {
updateEventCount();
updateDBSize();
resolve(request.result);
};
request.onerror = () => reject(request.error);
});
}
// Query events with compound indexes and cursor-based pagination
async function queryEvents(filters = {}, page = 1) {
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
let request;
let keyRange = null;
// Use compound index if both fields are specified
if (filters.source_app && filters.timestamp) {
const index = store.index('app_time');
keyRange = IDBKeyRange.bound(
[filters.source_app, filters.timestamp.start],
[filters.source_app, filters.timestamp.end]
);
request = index.openCursor(keyRange, 'prev');
} else if (filters.session_id && filters.hook_event_type) {
const index = store.index('session_type');
keyRange = IDBKeyRange.only([filters.session_id, filters.hook_event_type]);
request = index.openCursor(keyRange, 'prev');
} else if (filters.source_app) {
const index = store.index('source_app');
request = index.openCursor(IDBKeyRange.only(filters.source_app), 'prev');
} else if (filters.session_id) {
const index = store.index('session_id');
request = index.openCursor(IDBKeyRange.only(filters.session_id), 'prev');
} else if (filters.hook_event_type) {
const index = store.index('hook_event_type');
request = index.openCursor(IDBKeyRange.only(filters.hook_event_type), 'prev');
} else if (filters.timestamp) {
const index = store.index('timestamp');
keyRange = IDBKeyRange.bound(filters.timestamp.start, filters.timestamp.end);
request = index.openCursor(keyRange, 'prev');
} else {
request = store.openCursor(null, 'prev');
}
const results = [];
let count = 0;
const skip = (page - 1) * PAGE_SIZE;
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
count++;
// Skip to the right page
if (count > skip && results.length < PAGE_SIZE) {
results.push(cursor.value);
}
cursor.continue();
} else {
// Calculate total pages
totalPages = Math.ceil(count / PAGE_SIZE) || 1;
resolve({ results, total: count });
}
};
request.onerror = () => reject(request.error);
});
}
// Get aggregation statistics using indexes
async function getStatistics() {
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
const stats = {
byApp: {},
bySession: {},
byEventType: {},
total: 0
};
const request = store.openCursor();
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
const event = cursor.value;
stats.total++;
// Count by app
stats.byApp[event.source_app] = (stats.byApp[event.source_app] || 0) + 1;
// Count by session
stats.bySession[event.session_id] = (stats.bySession[event.session_id] || 0) + 1;
// Count by event type
stats.byEventType[event.hook_event_type] = (stats.byEventType[event.hook_event_type] || 0) + 1;
cursor.continue();
} else {
resolve(stats);
}
};
request.onerror = () => reject(request.error);
});
}
// Get all unique values for filters
async function getFilterOptions() {
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
const apps = new Set();
const sessions = new Set();
const request = store.openCursor();
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
apps.add(cursor.value.source_app);
sessions.add(cursor.value.session_id);
cursor.continue();
} else {
resolve({
apps: Array.from(apps).sort(),
sessions: Array.from(sessions).sort()
});
}
};
request.onerror = () => reject(request.error);
});
}
// Clear database
async function clearDatabase() {
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.clear();
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
// Export all events
async function exportEvents() {
const { results } = await queryEvents({}, 1);
const allEvents = [];
// Get all events using cursor
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.openCursor();
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
allEvents.push(cursor.value);
cursor.continue();
} else {
resolve(allEvents);
}
};
request.onerror = () => reject(request.error);
});
}
// WebSocket connection
function connectWebSocket() {
const url = document.getElementById('wsUrl').value;
ws = new WebSocket(url);
ws.onopen = () => {
updateStatus('ws', 'active', 'Connected');
document.getElementById('connectBtn').disabled = true;
document.getElementById('disconnectBtn').disabled = false;
};
ws.onmessage = async (event) => {
try {
const data = JSON.parse(event.data);
await addEvent(data);
// Refresh display if on first page
if (currentPage === 1) {
await executeQuery();
}
} catch (error) {
console.error('Error processing WebSocket message:', error);
}
};
ws.onerror = () => {
updateStatus('ws', 'error', 'Error');
};
ws.onclose = () => {
updateStatus('ws', 'inactive', 'Disconnected');
document.getElementById('connectBtn').disabled = false;
document.getElementById('disconnectBtn').disabled = true;
};
}
function disconnectWebSocket() {
if (ws) {
ws.close();
}
}
// Import events from file
async function importEvents(file) {
const text = await file.text();
const lines = text.split('\n').filter(line => line.trim());
let imported = 0;
for (const line of lines) {
try {
const event = JSON.parse(line);
await addEvent(event);
imported++;
} catch (error) {
console.error('Error importing event:', error);
}
}
alert(`Imported ${imported} events`);
await executeQuery();
await updateFilterOptions();
}
// Load sample data
async function loadSampleData() {
const eventTypes = ['PreToolUse', 'PostToolUse', 'Notification', 'Stop', 'UserPromptSubmit'];
const apps = ['demo-agent', 'test-agent', 'main-agent'];
const sessions = ['session-1', 'session-2', 'session-3'];
for (let i = 0; i < 100; i++) {
const event = {
source_app: apps[Math.floor(Math.random() * apps.length)],
session_id: sessions[Math.floor(Math.random() * sessions.length)],
hook_event_type: eventTypes[Math.floor(Math.random() * eventTypes.length)],
timestamp: new Date(Date.now() - Math.random() * 7 * 24 * 60 * 60 * 1000).toISOString(),
summary: `Sample event ${i + 1}`,
payload: { sample: true, index: i }
};
await addEvent(event);
}
alert('Loaded 100 sample events');
await executeQuery();
await updateFilterOptions();
}
// Execute query with filters
async function executeQuery() {
const filters = currentQuery;
const { results, total } = await queryEvents(filters, currentPage);
displayEvents(results);
updatePagination();
await updateStatistics();
}
// Build filters from UI
function buildFilters() {
const filters = {};
const app = document.getElementById('filterApp').value;
if (app) filters.source_app = app;
const session = document.getElementById('filterSession').value;
if (session) filters.session_id = session;
const eventType = document.getElementById('filterEventType').value;
if (eventType) filters.hook_event_type = eventType;
const timeRange = document.getElementById('filterTimeRange').value;
if (timeRange) {
const now = new Date();
let start;
if (timeRange === '1h') start = new Date(now - 60 * 60 * 1000);
else if (timeRange === '24h') start = new Date(now - 24 * 60 * 60 * 1000);
else if (timeRange === '7d') start = new Date(now - 7 * 24 * 60 * 60 * 1000);
if (start) {
filters.timestamp = {
start: start.toISOString(),
end: now.toISOString()
};
}
}
return filters;
}
// Display events
function displayEvents(events) {
const container = document.getElementById('eventList');
if (events.length === 0) {
container.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-secondary);">No events found matching the filters.</div>';
return;
}
container.innerHTML = events.map(event => `
<div class="event-item">
<div class="event-header">
<span class="event-type ${event.hook_event_type}">${event.hook_event_type}</span>
<span style="color: var(--text-secondary); font-size: 13px;">${new Date(event.timestamp).toLocaleString()}</span>
</div>
<div class="event-summary">${event.summary || 'No summary available'}</div>
<div class="event-meta">
<span>App: <strong>${event.source_app}</strong></span>
<span>Session: <strong>${event.session_id}</strong></span>
</div>
<div class="json-viewer" style="display: none;" id="payload-${event.id}">
${JSON.stringify(event.payload, null, 2)}
</div>
<button onclick="togglePayload(${event.id})" class="secondary" style="margin-top: 10px; font-size: 12px;">Toggle Payload</button>
</div>
`).join('');
}
// Toggle payload visibility
function togglePayload(id) {
const element = document.getElementById(`payload-${id}`);
element.style.display = element.style.display === 'none' ? 'block' : 'none';
}
// Update pagination
function updatePagination() {
document.getElementById('pageInfo').textContent = `Page ${currentPage} of ${totalPages}`;
document.getElementById('prevPageBtn').disabled = currentPage === 1;
document.getElementById('nextPageBtn').disabled = currentPage === totalPages;
}
// Update statistics
async function updateStatistics() {
const stats = await getStatistics();
const grid = document.getElementById('statsGrid');
grid.innerHTML = `
<div class="stat-card">
<div class="stat-value">${stats.total}</div>
<div class="stat-label">Total Events</div>
</div>
<div class="stat-card">
<div class="stat-value">${Object.keys(stats.byApp).length}</div>
<div class="stat-label">Apps</div>
</div>
<div class="stat-card">
<div class="stat-value">${Object.keys(stats.bySession).length}</div>
<div class="stat-label">Sessions</div>
</div>
<div class="stat-card">
<div class="stat-value">${Object.keys(stats.byEventType).length}</div>
<div class="stat-label">Event Types</div>
</div>
${Object.entries(stats.byEventType).map(([type, count]) => `
<div class="stat-card">
<div class="stat-value">${count}</div>
<div class="stat-label">${type}</div>
</div>
`).join('')}
`;
}
// Update filter options
async function updateFilterOptions() {
const { apps, sessions } = await getFilterOptions();
const appSelect = document.getElementById('filterApp');
const sessionSelect = document.getElementById('filterSession');
appSelect.innerHTML = '<option value="">All Apps</option>' +
apps.map(app => `<option value="${app}">${app}</option>`).join('');
sessionSelect.innerHTML = '<option value="">All Sessions</option>' +
sessions.map(session => `<option value="${session}">${session}</option>`).join('');
}
// Update event count
async function updateEventCount() {
const transaction = db.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.count();
request.onsuccess = () => {
document.getElementById('eventCount').textContent = request.result;
};
}
// Estimate database size
async function updateDBSize() {
if (navigator.storage && navigator.storage.estimate) {
const estimate = await navigator.storage.estimate();
const sizeMB = (estimate.usage / (1024 * 1024)).toFixed(2);
document.getElementById('dbSize').textContent = `${sizeMB} MB`;
}
}
// Update status indicators
function updateStatus(type, status, text) {
const indicator = document.getElementById(`${type}Status`);
const textElement = document.getElementById(`${type}StatusText`);
indicator.className = 'status-indicator ' + status;
textElement.textContent = text;
}
// Event listeners
document.getElementById('connectBtn').addEventListener('click', connectWebSocket);
document.getElementById('disconnectBtn').addEventListener('click', disconnectWebSocket);
document.getElementById('importBtn').addEventListener('click', () => {
const file = document.getElementById('fileInput').files[0];
if (file) importEvents(file);
});
document.getElementById('exportBtn').addEventListener('click', async () => {
const events = await exportEvents();
const blob = new Blob([JSON.stringify(events, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `hook-events-${new Date().toISOString()}.json`;
a.click();
});
document.getElementById('clearDbBtn').addEventListener('click', async () => {
if (confirm('Clear all events from database?')) {
await clearDatabase();
await executeQuery();
await updateStatistics();
await updateFilterOptions();
}
});
document.getElementById('sampleDataBtn').addEventListener('click', loadSampleData);
document.getElementById('refreshStatsBtn').addEventListener('click', updateStatistics);
document.getElementById('queryBtn').addEventListener('click', () => {
currentPage = 1;
currentQuery = buildFilters();
executeQuery();
});
document.getElementById('resetFiltersBtn').addEventListener('click', () => {
document.getElementById('filterApp').value = '';
document.getElementById('filterSession').value = '';
document.getElementById('filterEventType').value = '';
document.getElementById('filterTimeRange').value = '';
currentPage = 1;
currentQuery = {};
executeQuery();
});
document.getElementById('prevPageBtn').addEventListener('click', () => {
if (currentPage > 1) {
currentPage--;
executeQuery();
}
});
document.getElementById('nextPageBtn').addEventListener('click', () => {
if (currentPage < totalPages) {
currentPage++;
executeQuery();
}
});
// Initialize on load
(async () => {
try {
await initDB();
await executeQuery();
await updateStatistics();
await updateFilterOptions();
await updateEventCount();
await updateDBSize();
} catch (error) {
console.error('Initialization error:', error);
alert('Failed to initialize database: ' + error.message);
}
})();
</script>
</body>
</html>