Complete real-time monitoring dashboard for Claude Code Features: - FastAPI backend with REST API and WebSocket - React + TypeScript frontend with dark theme - SQLite database for event storage - 6 hook scripts for Claude Code events - Docker deployment setup - Real-time event tracking and statistics - Event filtering (ALL, TOOLS, AGENTS, PROMPTS, SESSIONS) - Connection status indicator - Reset stats functionality Tech Stack: - Backend: Python 3.11, FastAPI, SQLAlchemy, WebSockets - Frontend: React 18, TypeScript, Vite - Database: SQLite - Deployment: Docker, Docker Compose 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
247 lines
6.9 KiB
Python
247 lines
6.9 KiB
Python
"""CRUD operations for database access."""
|
|
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import desc, func, or_
|
|
from typing import List, Optional, Tuple
|
|
import time
|
|
import json
|
|
|
|
from app.database import Event, Session as SessionModel, Statistics, ToolUsage
|
|
from app.schemas import EventCreate
|
|
|
|
|
|
def create_event(db: Session, event: EventCreate) -> Event:
|
|
"""
|
|
Create a new event and update statistics.
|
|
|
|
Args:
|
|
db: Database session
|
|
event: Event data to create
|
|
|
|
Returns:
|
|
Created event object
|
|
"""
|
|
# Convert tool_input to JSON string if it's a dict
|
|
tool_input_str = None
|
|
if event.tool_input is not None:
|
|
if isinstance(event.tool_input, str):
|
|
tool_input_str = event.tool_input
|
|
else:
|
|
tool_input_str = json.dumps(event.tool_input)
|
|
|
|
# Generate description if not provided
|
|
description = event.description
|
|
if not description:
|
|
description = _generate_description(event)
|
|
|
|
# Create event
|
|
db_event = Event(
|
|
session_id=event.session_id,
|
|
event_type=event.event_type,
|
|
tool_name=event.tool_name,
|
|
tool_input=tool_input_str,
|
|
tool_output=event.tool_output,
|
|
success=event.success,
|
|
description=description,
|
|
timestamp=event.timestamp,
|
|
)
|
|
db.add(db_event)
|
|
|
|
# Update or create session
|
|
session = db.query(SessionModel).filter(SessionModel.session_id == event.session_id).first()
|
|
if not session:
|
|
session = SessionModel(
|
|
session_id=event.session_id,
|
|
started_at=event.timestamp,
|
|
event_count=1,
|
|
)
|
|
db.add(session)
|
|
else:
|
|
session.event_count += 1
|
|
if event.event_type == "SessionEnd":
|
|
session.ended_at = event.timestamp
|
|
|
|
# Update tool usage for tool events
|
|
if event.tool_name:
|
|
tool_usage = db.query(ToolUsage).filter(ToolUsage.tool_name == event.tool_name).first()
|
|
if not tool_usage:
|
|
tool_usage = ToolUsage(
|
|
tool_name=event.tool_name,
|
|
usage_count=1,
|
|
first_used=event.timestamp,
|
|
last_used=event.timestamp,
|
|
)
|
|
db.add(tool_usage)
|
|
else:
|
|
tool_usage.usage_count += 1
|
|
tool_usage.last_used = event.timestamp
|
|
|
|
# Update statistics
|
|
_update_statistics(db)
|
|
|
|
db.commit()
|
|
db.refresh(db_event)
|
|
return db_event
|
|
|
|
|
|
def get_events(
|
|
db: Session,
|
|
skip: int = 0,
|
|
limit: int = 50,
|
|
event_type: Optional[str] = None,
|
|
session_id: Optional[str] = None,
|
|
tool_name: Optional[str] = None,
|
|
) -> Tuple[List[Event], int]:
|
|
"""
|
|
Get paginated list of events with optional filtering.
|
|
|
|
Args:
|
|
db: Database session
|
|
skip: Number of records to skip (offset)
|
|
limit: Maximum number of records to return
|
|
event_type: Filter by event type
|
|
session_id: Filter by session ID
|
|
tool_name: Filter by tool name
|
|
|
|
Returns:
|
|
Tuple of (events list, total count)
|
|
"""
|
|
query = db.query(Event)
|
|
|
|
# Apply filters
|
|
if event_type:
|
|
# Support filtering by multiple event types (comma-separated)
|
|
if "," in event_type:
|
|
event_types = [et.strip() for et in event_type.split(",")]
|
|
query = query.filter(Event.event_type.in_(event_types))
|
|
else:
|
|
query = query.filter(Event.event_type == event_type)
|
|
|
|
if session_id:
|
|
query = query.filter(Event.session_id == session_id)
|
|
|
|
if tool_name:
|
|
query = query.filter(Event.tool_name == tool_name)
|
|
|
|
# Get total count
|
|
total = query.count()
|
|
|
|
# Order by timestamp descending (newest first) and apply pagination
|
|
events = query.order_by(desc(Event.timestamp)).offset(skip).limit(limit).all()
|
|
|
|
return events, total
|
|
|
|
|
|
def get_event_by_id(db: Session, event_id: int) -> Optional[Event]:
|
|
"""Get a single event by ID."""
|
|
return db.query(Event).filter(Event.id == event_id).first()
|
|
|
|
|
|
def delete_all_events(db: Session) -> None:
|
|
"""Delete all events and reset statistics (for Reset Stats functionality)."""
|
|
# Delete all events
|
|
db.query(Event).delete()
|
|
|
|
# Delete all sessions
|
|
db.query(SessionModel).delete()
|
|
|
|
# Delete all tool usage
|
|
db.query(ToolUsage).delete()
|
|
|
|
# Reset statistics
|
|
stats = db.query(Statistics).first()
|
|
if stats:
|
|
stats.total_events = 0
|
|
stats.total_tools_used = 0
|
|
stats.total_agents = 0
|
|
stats.total_sessions = 0
|
|
stats.last_updated = time.time()
|
|
|
|
db.commit()
|
|
|
|
|
|
def get_statistics(db: Session) -> Statistics:
|
|
"""Get current statistics."""
|
|
stats = db.query(Statistics).first()
|
|
if not stats:
|
|
# Initialize if not exists
|
|
stats = Statistics(
|
|
id=1,
|
|
total_events=0,
|
|
total_tools_used=0,
|
|
total_agents=0,
|
|
total_sessions=0,
|
|
last_updated=time.time(),
|
|
)
|
|
db.add(stats)
|
|
db.commit()
|
|
db.refresh(stats)
|
|
return stats
|
|
|
|
|
|
def get_tool_usage(db: Session) -> List[ToolUsage]:
|
|
"""Get tool usage statistics."""
|
|
return db.query(ToolUsage).order_by(desc(ToolUsage.usage_count)).all()
|
|
|
|
|
|
def get_sessions(db: Session, limit: int = 100) -> List[SessionModel]:
|
|
"""Get list of sessions."""
|
|
return db.query(SessionModel).order_by(desc(SessionModel.started_at)).limit(limit).all()
|
|
|
|
|
|
# Helper functions
|
|
def _update_statistics(db: Session) -> None:
|
|
"""Update cached statistics."""
|
|
stats = db.query(Statistics).first()
|
|
if not stats:
|
|
stats = Statistics(id=1)
|
|
db.add(stats)
|
|
|
|
# Count events
|
|
stats.total_events = db.query(Event).count()
|
|
|
|
# Count unique tools
|
|
stats.total_tools_used = db.query(ToolUsage).count()
|
|
|
|
# Count agents (SubagentStop events)
|
|
stats.total_agents = db.query(Event).filter(Event.event_type == "SubagentStop").count()
|
|
|
|
# Count sessions
|
|
stats.total_sessions = db.query(SessionModel).count()
|
|
|
|
# Update timestamp
|
|
stats.last_updated = time.time()
|
|
|
|
|
|
def _generate_description(event: EventCreate) -> str:
|
|
"""Generate a human-readable description for an event."""
|
|
if event.description:
|
|
return event.description
|
|
|
|
# Generate description based on event type
|
|
if event.event_type == "PreToolUse":
|
|
if event.tool_name:
|
|
return f"Preparing to use {event.tool_name}"
|
|
return "Preparing to use tool"
|
|
|
|
elif event.event_type == "PostToolUse":
|
|
if event.tool_name:
|
|
status = "successfully" if event.success else "with errors"
|
|
return f"Executed {event.tool_name} {status}"
|
|
return "Tool execution completed"
|
|
|
|
elif event.event_type == "SessionStart":
|
|
return "Session started"
|
|
|
|
elif event.event_type == "SessionEnd":
|
|
return "Session ended"
|
|
|
|
elif event.event_type == "SubagentStop":
|
|
return "Agent completed task"
|
|
|
|
elif event.event_type == "UserPromptSubmit":
|
|
return "User submitted prompt"
|
|
|
|
else:
|
|
return f"{event.event_type} event"
|