Files
claude-code-monitor/backend/app/crud.py
felix.zoesch f6ef7ff5d3 Initial commit: Claude Code Monitor v1.0.0
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>
2025-12-15 09:49:06 +01:00

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"